{"openapi":"3.0.3","info":{"title":"Social Relay API","version":"0.2.0","description":"Social Relay — multi-tenant social publishing middle layer (Facebook, Instagram, LinkedIn, Bluesky, Mastodon).\n\n**Tenancy model:** A **platform** (SaaS buyer) owns multiple **applications** (product isolation units: e.g. Timely vs SoundCloud). API keys, profiles, and webhooks are bound to one application. Keys cannot see or mutate another application's data.\n\n**Auth:** Bearer platform API key (`sk_live_…`) for product REST; dashboard session JWT for console; Entra app roles for ops admin.\n\n**Isolation headers:**\n- `X-Application-Id` — required for dashboard sessions on app-scoped writes; optional for keys (key already carries applicationId).\n- `X-Profile-Id` — required for profile-scoped routes (posts, connections).\n- `X-Profile-Grant` — short-lived JWT for restricted keys acting outside their static allowlist.\n\nPublic domain socialrelay.dev · OpenAPI at `/api/v1/openapi.json`."},"servers":[{"url":"https://api.socialrelay.dev/api","description":"Production (Social Relay)"},{"url":"https://func-social-relay-prod.azurewebsites.net/api","description":"Azure default host"},{"url":"http://localhost:7071/api","description":"Local"}],"tags":[{"name":"meta"},{"name":"platform"},{"name":"applications","description":"Product isolation units under a platform (keys/profiles/webhooks bound here)"},{"name":"keys","description":"Application-scoped API keys (app_admin or restricted)"},{"name":"profiles"},{"name":"posts"},{"name":"connect"},{"name":"billing"},{"name":"webhooks"},{"name":"admin"}],"components":{"securitySchemes":{"PlatformApiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_…","description":"Application-scoped platform API key. Bound to exactly one application; cannot cross apps."},"EntraBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Entra access token with app role Admin or Operator"}},"parameters":{"ProfileIdHeader":{"name":"X-Profile-Id","in":"header","required":false,"schema":{"type":"string","pattern":"^prf_"},"description":"Required for profile-scoped routes. Profile must belong to the same application as the API key (or session X-Application-Id)."},"ApplicationIdHeader":{"name":"X-Application-Id","in":"header","required":false,"schema":{"type":"string","pattern":"^app_"},"description":"Dashboard sessions: select which application to operate on. API keys ignore this for isolation (key's application wins). Pattern: app_…"},"ProfileGrantHeader":{"name":"X-Profile-Grant","in":"header","required":false,"schema":{"type":"string"},"description":"Short-lived grant JWT from POST /v1/profile-grants. Required when a restricted key acts on a profile not on its allowlist."},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object"},"requestId":{"type":"string"}}}}},"DataEnvelope":{"type":"object","properties":{"data":{}}},"Application":{"type":"object","properties":{"id":{"type":"string","pattern":"^app_","example":"app_01KZ7HG9N7CF093GWPK2P1WXKA"},"name":{"type":"string","example":"Timely"},"status":{"type":"string","enum":["active","disabled"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationCreated":{"allOf":[{"$ref":"#/components/schemas/Application"},{"type":"object","properties":{"apiKey":{"nullable":true,"description":"Present when createDefaultKey is true (default). Secret only on this response.","type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"keyPrefix":{"type":"string"},"scope":{"type":"string","enum":["app_admin","restricted"]},"applicationId":{"type":"string"},"secret":{"type":"string","description":"Full sk_live_… secret — store now; never returned again"}}}}}]},"ApiKey":{"type":"object","properties":{"id":{"type":"string","pattern":"^key_"},"applicationId":{"type":"string","pattern":"^app_"},"name":{"type":"string"},"scope":{"type":"string","enum":["app_admin","restricted"]},"allowedProfileIds":{"type":"array","items":{"type":"string"},"description":"Static allowlist for restricted keys; empty for app_admin"},"keyPrefix":{"type":"string","description":"Public prefix for display (not the secret)"},"status":{"type":"string","enum":["active","revoked","expired"]},"expiresAt":{"type":"string","format":"date-time","nullable":true},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeyCreated":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Full secret — shown once"}}}]},"Me":{"type":"object","properties":{"id":{"type":"string","pattern":"^plt_"},"name":{"type":"string"},"status":{"type":"string"},"tokenBalance":{"type":"integer"},"tokenReserved":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"application":{"nullable":true,"description":"Current application context (from API key binding or X-Application-Id)","type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}}},"applications":{"type":"array","description":"All applications on this platform (API key callers only receive their own app on list endpoints; me still lists platform apps for dashboard)","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"}}}},"counts":{"type":"object","properties":{"profiles":{"type":"integer"},"activeApiKeys":{"type":"integer"},"webhooks":{"type":"integer"},"connections":{"type":"integer"},"applications":{"type":"integer"}}},"auth":{"type":"object","properties":{"kind":{"type":"string","enum":["api_key","session"]},"apiKeyId":{"type":"string","nullable":true},"userId":{"type":"string","nullable":true},"applicationId":{"type":"string","nullable":true}}}}},"ProfileGrant":{"type":"object","properties":{"token":{"type":"string","description":"Pass as X-Profile-Grant header"},"profileId":{"type":"string"},"applicationId":{"type":"string"},"expiresIn":{"type":"integer","description":"Seconds"},"expiresAt":{"type":"string","format":"date-time"},"header":{"type":"string","example":"X-Profile-Grant"}}}}},"paths":{"/health":{"get":{"tags":["meta"],"summary":"Liveness","responses":{"200":{"description":"OK"}}}},"/ready":{"get":{"tags":["meta"],"summary":"Readiness (includes SQL ping when configured)","responses":{"200":{"description":"Ready"},"503":{"description":"Not ready"}}}},"/v1/openapi.json":{"get":{"tags":["meta"],"summary":"OpenAPI document","responses":{"200":{"description":"OpenAPI 3 JSON"}}}},"/v1/me":{"get":{"tags":["platform"],"security":[{"PlatformApiKey":[]}],"summary":"Current platform account","description":"Returns platform snapshot plus `application` (active context), `applications` list, and scoped `counts` (profiles/keys/webhooks filtered to the current application when one is set).","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"200":{"description":"Platform snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Me"}}}}}},"401":{"description":"Unauthorized"}}}},"/v1/applications":{"get":{"tags":["applications"],"security":[{"PlatformApiKey":[]}],"summary":"List applications","description":"Dashboard session: all apps on the platform. API key: only the key's bound application.","responses":{"200":{"description":"Application list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Application"}}}}}}}}},"post":{"tags":["applications"],"security":[{"PlatformApiKey":[]}],"summary":"Create application","description":"Dashboard session only. By default mints an app_admin API key bound to the new application (`apiKey.secret` returned once).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"createDefaultKey":{"type":"boolean","default":true,"description":"When true (default), mint an app_admin key; secret returned once on this response"},"defaultKeyName":{"type":"string","maxLength":80,"description":"Label for the default key (default: \"{name} default\")"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApplicationCreated"}}}}}},"403":{"description":"API keys cannot create applications"}}}},"/v1/keys":{"get":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"List API keys for the current application","description":"API key callers: keys for their application only. Session: pass X-Application-Id to scope; omit for platform-wide list (dashboard).","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"200":{"description":"Key list (no secrets)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}}}},"post":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"Create application-scoped API key","description":"Secret returned once. Keys cannot mint for another application.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"expiresAt":{"type":"string","format":"date-time","nullable":true,"description":"ISO-8601; omit/null for no expiry"},"scope":{"type":"string","enum":["app_admin","restricted"],"default":"app_admin","description":"app_admin: all profiles in the app. restricted: only allowedProfileIds (+ optional X-Profile-Grant)."},"allowedProfileIds":{"type":"array","items":{"type":"string"},"maxItems":500,"description":"Required/used when scope is restricted"},"applicationId":{"type":"string","description":"Dashboard: target app. API key: must match key's app if set."}}}}}},"responses":{"201":{"description":"Created (secret once)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreated"}}}}}}}}},"/v1/keys/{keyId}":{"patch":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"Rename API key","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80}}}}}},"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"Hard-delete API key","description":"Permanently removes the key row (hash + metadata). Prefer POST .../revoke to soft-disable while keeping an audit row.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/v1/keys/{keyId}/revoke":{"post":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"Soft-revoke API key","description":"Sets status=revoked; row kept for audit. Use DELETE for permanent purge.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked"},"404":{"description":"Not found"}}}},"/v1/profile-grants":{"post":{"tags":["keys"],"security":[{"PlatformApiKey":[]}],"summary":"Mint short-lived profile act-as grant","description":"Only app_admin keys or dashboard sessions. Restricted keys present the token as X-Profile-Grant to act outside their static allowlist.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["profileId"],"properties":{"profileId":{"type":"string","pattern":"^prf_"},"expiresIn":{"type":"integer","minimum":60,"maximum":7200,"default":900,"description":"Seconds until expiry"}}}}}},"responses":{"201":{"description":"Grant minted","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProfileGrant"}}}}}},"403":{"description":"Restricted keys cannot mint grants"}}}},"/v1/rate-card":{"get":{"tags":["platform"],"security":[{"PlatformApiKey":[]}],"summary":"Token rate card","responses":{"200":{"description":"Rate card"}}}},"/v1/audit":{"get":{"tags":["platform"],"security":[{"PlatformApiKey":[]}],"summary":"Recent audit events for this platform (process-local ring buffer)","responses":{"200":{"description":"Audit list"}}}},"/v1/profiles":{"get":{"tags":["profiles"],"security":[{"PlatformApiKey":[]}],"summary":"List profiles","description":"Scoped to the API key's application (or session X-Application-Id). Restricted keys only see allowlisted profiles.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"200":{"description":"Profile list"}}},"post":{"tags":["profiles"],"security":[{"PlatformApiKey":[]}],"summary":"Create profile","description":"Created under the caller's application.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"201":{"description":"Created"}}}},"/v1/profiles/{profileId}":{"get":{"tags":["profiles"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Profile"}}},"patch":{"tags":["profiles"],"security":[{"PlatformApiKey":[]}],"summary":"Update profile (title, externalId, metadata, status)","description":"Set or clear externalId (buyer customer/org id). null or empty string clears. Must be unique per application when set.","parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated"}}},"delete":{"tags":["profiles"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"}}],"summary":"Soft-delete (disable) profile","responses":{"200":{"description":"Disabled"}}}},"/v1/posts/validate":{"post":{"tags":["posts"],"security":[{"PlatformApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/ProfileIdHeader"},{"$ref":"#/components/parameters/ProfileGrantHeader"}],"summary":"Validate post (network rules + optional media HEAD preflight)","responses":{"200":{"description":"Validation result"}}}},"/v1/posts":{"get":{"tags":["posts"],"security":[{"PlatformApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/ProfileIdHeader"},{"$ref":"#/components/parameters/ProfileGrantHeader"}],"responses":{"200":{"description":"Post list"}}},"post":{"tags":["posts"],"security":[{"PlatformApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/ProfileIdHeader"},{"$ref":"#/components/parameters/ProfileGrantHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"summary":"Create post (queues publish)","responses":{"201":{"description":"Scheduled"},"202":{"description":"Accepted"}}}},"/v1/posts/{postId}":{"get":{"tags":["posts"],"security":[{"PlatformApiKey":[]}],"parameters":[{"$ref":"#/components/parameters/ProfileIdHeader"},{"$ref":"#/components/parameters/ProfileGrantHeader"},{"name":"postId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Post + legs"}}}},"/v1/profiles/{profileId}/connections":{"get":{"tags":["connect"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Connections"}}}},"/v1/profiles/{profileId}/connect/sessions":{"post":{"tags":["connect"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"profileId","in":"path","required":true,"schema":{"type":"string"}}],"summary":"Create connect session (returns branded connectUrl)","responses":{"201":{"description":"Session created"}}}},"/v1/connect/ui":{"get":{"tags":["connect"],"summary":"Hosted white-label connect landing (end-user browser)","responses":{"200":{"description":"HTML"}}}},"/v1/connect/sessions/{sessionId}/cancel":{"post":{"tags":["connect"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"sessionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled"}}}},"/v1/billing/balance":{"get":{"tags":["billing"],"security":[{"PlatformApiKey":[]}],"responses":{"200":{"description":"Balance"}}}},"/v1/billing/usage":{"get":{"tags":["billing"],"security":[{"PlatformApiKey":[]}],"responses":{"200":{"description":"Ledger"}}}},"/v1/billing/admin/credit":{"post":{"tags":["admin"],"security":[{"EntraBearer":[]}],"summary":"Admin token credit (Entra role Admin)","responses":{"201":{"description":"Credited"}}}},"/v1/webhooks":{"get":{"tags":["webhooks"],"security":[{"PlatformApiKey":[]}],"summary":"List webhook endpoints for the current application","description":"Fan-out is application-scoped: only endpoints on the same application as the post's profile receive events.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"200":{"description":"Webhook list"}}},"post":{"tags":["webhooks"],"security":[{"PlatformApiKey":[]}],"summary":"Register webhook (application-scoped)","description":"HTTPS only. Bound to the caller's application.","parameters":[{"$ref":"#/components/parameters/ApplicationIdHeader"}],"responses":{"201":{"description":"Created (secret once)"}}}},"/v1/webhooks/{webhookId}":{"delete":{"tags":["webhooks"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted"}}}},"/v1/webhooks/{webhookId}/deliveries":{"get":{"tags":["webhooks"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery log"}}}},"/v1/webhooks/{webhookId}/deliveries/{deliveryId}/redrive":{"post":{"tags":["webhooks"],"security":[{"PlatformApiKey":[]}],"parameters":[{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}},{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Redrive result"}}}},"/v1/admin/platforms":{"get":{"tags":["admin"],"security":[{"EntraBearer":[]}],"summary":"List platform accounts (Admin or Operator)","responses":{"200":{"description":"Platforms"}}}},"/v1/admin/platforms/{platformId}":{"get":{"tags":["admin"],"security":[{"EntraBearer":[]}],"parameters":[{"name":"platformId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Platform"}}}},"/v1/admin/webhooks/redrive-dead":{"post":{"tags":["admin"],"security":[{"EntraBearer":[]}],"summary":"Redrive dead webhook deliveries (Admin)","responses":{"200":{"description":"Counts"}}}},"/v1/admin/audit":{"get":{"tags":["admin"],"security":[{"EntraBearer":[]}],"summary":"Recent process-local audit events (Admin or Operator)","responses":{"200":{"description":"Audit list"}}}}}}