{"openapi":"3.1.0","info":{"title":"Per My Last Webhook API","version":"0.1.0","description":"Receive build/deploy/alert events from 16 CI and monitoring platforms (plus legacy Office 365 MessageCards and generic JSON) and post rich Adaptive Cards into Microsoft Teams."},"servers":[{"url":"{publicBaseUrl}","variables":{"publicBaseUrl":{"default":"http://localhost:7071"}}}],"components":{"securitySchemes":{"functionKey":{"type":"apiKey","in":"header","name":"x-functions-key"},"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"},"orgId":{"type":"apiKey","in":"header","name":"x-org-id","description":"Refused once ENTRA_CLIENT_ID is configured."},"entraBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Entra ID access token from an installed tenant carrying the Connector.Admin app role. Replaces x-org-id when present."},"botFrameworkJwt":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Bot Framework service token (issuer api.botframework.com, audience = bot app id)."},"stripeSignature":{"type":"apiKey","in":"header","name":"Stripe-Signature","description":"HMAC-SHA256 over `t.<raw body>` with the webhook secret; 5-minute tolerance."}},"schemas":{"Platform":{"type":"string","enum":["jenkins","github","gitlab","azuredevops","buildkite","circleci","bitbucket","argocd","teamcity","datadog","grafana","drone","sentry","pagerduty","alertmanager","octopus","messagecard","generic"]},"BuildStatus":{"type":"string","enum":["started","success","failure","cancelled","unstable","deployment_started","deployment_success","deployment_failure"]},"Mention":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","minLength":1,"maxLength":256,"description":"Teams MRI or AAD object id."},"name":{"type":"string","minLength":1,"maxLength":128}}},"BotChannel":{"type":"object","description":"Bot Framework channel destination. Mutually exclusive with teamsWebhookUrl and dmUser.","required":["serviceUrl","conversationId"],"properties":{"serviceUrl":{"type":"string","format":"uri","description":"Must be a Microsoft Bot Framework host."},"conversationId":{"type":"string","minLength":1,"maxLength":512},"displayName":{"type":"string","maxLength":128}}},"DmUser":{"type":"object","description":"Bot Framework 1:1 destination. Mutually exclusive with teamsWebhookUrl and botChannel. Pro plan.","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":256},"name":{"type":"string","minLength":1,"maxLength":128}}},"RouteInput":{"type":"object","required":["match"],"description":"Exactly one of botChannel, teamsWebhookUrl, or dmUser must be set.","properties":{"match":{"type":"object","properties":{"pipeline":{"type":"string","minLength":1,"maxLength":256,"description":"Glob."},"branch":{"type":"string","minLength":1,"maxLength":256,"description":"Glob."},"environment":{"type":"string","minLength":1,"maxLength":128},"status":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/BuildStatus"}}}},"botChannel":{"type":"object","description":"Bot Framework channel destination. Mutually exclusive with teamsWebhookUrl and dmUser.","required":["serviceUrl","conversationId"],"properties":{"serviceUrl":{"type":"string","format":"uri","description":"Must be a Microsoft Bot Framework host."},"conversationId":{"type":"string","minLength":1,"maxLength":512},"displayName":{"type":"string","maxLength":128}}},"teamsWebhookUrl":{"type":"string","format":"uri","writeOnly":true,"description":"Teams Workflows or Incoming Webhook HTTPS URL. Mutually exclusive with botChannel and dmUser. Write-only: never returned by any response — see hasTeamsWebhookUrl."},"dmUser":{"type":"object","description":"Bot Framework 1:1 destination. Mutually exclusive with teamsWebhookUrl and botChannel. Pro plan.","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":256},"name":{"type":"string","minLength":1,"maxLength":128}}}}},"Route":{"allOf":[{"$ref":"#/components/schemas/RouteInput"},{"type":"object","properties":{"hasTeamsWebhookUrl":{"type":"boolean","description":"Present and true when the route has a webhook URL (which is never returned)."}}}]},"SubscriptionInput":{"type":"object","required":["name","platform"],"description":"Exactly one of teamsWebhookUrl, botChannel, or dmUser is required. Generic subscriptions also require a mapping with at least one of title/status/link/summary.","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"platform":{"$ref":"#/components/schemas/Platform"},"teamsWebhookUrl":{"type":"string","format":"uri","writeOnly":true,"description":"Teams Workflows or Incoming Webhook HTTPS URL. Mutually exclusive with botChannel and dmUser. Write-only: never returned by any response — see hasTeamsWebhookUrl."},"signingSecret":{"type":"string","minLength":8,"maxLength":256,"writeOnly":true,"description":"Per-subscription webhook signing secret: configure the same value in the platform (GitHub Secret, GitLab Secret token, Azure DevOps basic-auth password, an `Authorization: Bearer` header, …) and the ingest preflight for this subscription verifies every delivery against it — a missing or invalid signature is a 401. Falls back to the deployment env var (self-host / broadcast route) when unset. Write-only: never returned by any response — see hasSigningSecret. On PUT an omitted value clears it."},"botChannel":{"type":"object","description":"Bot Framework channel destination. Mutually exclusive with teamsWebhookUrl and dmUser.","required":["serviceUrl","conversationId"],"properties":{"serviceUrl":{"type":"string","format":"uri","description":"Must be a Microsoft Bot Framework host."},"conversationId":{"type":"string","minLength":1,"maxLength":512},"displayName":{"type":"string","maxLength":128}}},"dmUser":{"type":"object","description":"Bot Framework 1:1 destination. Mutually exclusive with teamsWebhookUrl and botChannel. Pro plan.","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":256},"name":{"type":"string","minLength":1,"maxLength":128}}},"pipelineAllowList":{"type":"array","maxItems":200,"items":{"type":"string","minLength":1,"maxLength":256}},"statusFilter":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/BuildStatus"}},"branding":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","description":"HTTPS logo shown in card headers. Pro plan."}}},"incidentMode":{"type":"boolean","description":"Collapse repeated failures into an updating card for bot-channel destinations. Pro plan."},"quietHours":{"type":"object","required":["start","end"],"description":"Pro plan.","properties":{"start":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"22:00"},"end":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"07:00"},"tz":{"type":"string","maxLength":64,"default":"UTC","example":"America/Los_Angeles"}}},"dedupeWindowSec":{"type":"integer","minimum":0,"maximum":86400,"description":"Pro plan when > 0."},"failureAfterSuccessOnly":{"type":"boolean","description":"Pro plan."},"mentionsOnFailure":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Mention"},"description":"Pro plan."},"routes":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/RouteInput"},"description":"Pro plan."},"mapping":{"type":"object","description":"Field mapping used by generic-platform subscriptions (dot-paths into the payload).","properties":{"title":{"type":"string","maxLength":256},"status":{"type":"string","maxLength":256},"link":{"type":"string","maxLength":256},"summary":{"type":"string","maxLength":256},"facts":{"type":"array","maxItems":20,"items":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","minLength":1,"maxLength":80},"value":{"type":"string","minLength":1,"maxLength":256}}}},"statusMap":{"type":"object","additionalProperties":{"type":"string","enum":["success","failure","running","neutral"]}}}},"enabled":{"type":"boolean","default":true}}},"SubscriptionPatch":{"type":"object","description":"Every field optional. An empty object is accepted and is a no-op that still bumps updatedAt. Setting exactly one destination replaces the current one; `platform` cannot change. `signingSecret: null` clears the stored signing secret.","properties":{"name":{"type":"string","minLength":1,"maxLength":128},"platform":{"$ref":"#/components/schemas/Platform"},"teamsWebhookUrl":{"type":"string","format":"uri","writeOnly":true,"description":"Teams Workflows or Incoming Webhook HTTPS URL. Mutually exclusive with botChannel and dmUser. Write-only: never returned by any response — see hasTeamsWebhookUrl."},"signingSecret":{"type":"string","minLength":8,"maxLength":256,"writeOnly":true,"description":"Per-subscription webhook signing secret: configure the same value in the platform (GitHub Secret, GitLab Secret token, Azure DevOps basic-auth password, an `Authorization: Bearer` header, …) and the ingest preflight for this subscription verifies every delivery against it — a missing or invalid signature is a 401. Falls back to the deployment env var (self-host / broadcast route) when unset. Write-only: never returned by any response — see hasSigningSecret. On PUT an omitted value clears it. Send null to clear.","nullable":true},"botChannel":{"type":"object","description":"Bot Framework channel destination. Mutually exclusive with teamsWebhookUrl and dmUser.","required":["serviceUrl","conversationId"],"properties":{"serviceUrl":{"type":"string","format":"uri","description":"Must be a Microsoft Bot Framework host."},"conversationId":{"type":"string","minLength":1,"maxLength":512},"displayName":{"type":"string","maxLength":128}}},"dmUser":{"type":"object","description":"Bot Framework 1:1 destination. Mutually exclusive with teamsWebhookUrl and botChannel. Pro plan.","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":256},"name":{"type":"string","minLength":1,"maxLength":128}}},"pipelineAllowList":{"type":"array","maxItems":200,"items":{"type":"string","minLength":1,"maxLength":256}},"statusFilter":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/BuildStatus"}},"branding":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","description":"HTTPS logo shown in card headers. Pro plan."}}},"incidentMode":{"type":"boolean","description":"Collapse repeated failures into an updating card for bot-channel destinations. Pro plan."},"quietHours":{"type":"object","required":["start","end"],"description":"Pro plan.","properties":{"start":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"22:00"},"end":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"07:00"},"tz":{"type":"string","maxLength":64,"default":"UTC","example":"America/Los_Angeles"}}},"dedupeWindowSec":{"type":"integer","minimum":0,"maximum":86400,"description":"Pro plan when > 0."},"failureAfterSuccessOnly":{"type":"boolean","description":"Pro plan."},"mentionsOnFailure":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Mention"},"description":"Pro plan."},"routes":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/RouteInput"},"description":"Pro plan."},"mapping":{"type":"object","description":"Field mapping used by generic-platform subscriptions (dot-paths into the payload).","properties":{"title":{"type":"string","maxLength":256},"status":{"type":"string","maxLength":256},"link":{"type":"string","maxLength":256},"summary":{"type":"string","maxLength":256},"facts":{"type":"array","maxItems":20,"items":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","minLength":1,"maxLength":80},"value":{"type":"string","minLength":1,"maxLength":256}}}},"statusMap":{"type":"object","additionalProperties":{"type":"string","enum":["success","failure","running","neutral"]}}}},"enabled":{"type":"boolean","default":true}}},"Subscription":{"type":"object","description":"A subscription as returned by the API. `teamsWebhookUrl` and `signingSecret` are never returned; `hasTeamsWebhookUrl` / `hasSigningSecret` say whether each is set.","required":["id","orgId","name","platform","enabled","createdAt","updatedAt","hasTeamsWebhookUrl","hasSigningSecret","ingestUrl"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string"},"name":{"type":"string","minLength":1,"maxLength":128},"platform":{"$ref":"#/components/schemas/Platform"},"hasTeamsWebhookUrl":{"type":"boolean"},"hasSigningSecret":{"type":"boolean","description":"True when a per-subscription signing secret is stored (the value is never returned)."},"botChannel":{"type":"object","description":"Bot Framework channel destination. Mutually exclusive with teamsWebhookUrl and dmUser.","required":["serviceUrl","conversationId"],"properties":{"serviceUrl":{"type":"string","format":"uri","description":"Must be a Microsoft Bot Framework host."},"conversationId":{"type":"string","minLength":1,"maxLength":512},"displayName":{"type":"string","maxLength":128}}},"dmUser":{"type":"object","description":"Bot Framework 1:1 destination. Mutually exclusive with teamsWebhookUrl and botChannel. Pro plan.","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":256},"name":{"type":"string","minLength":1,"maxLength":128}}},"pipelineAllowList":{"type":"array","maxItems":200,"items":{"type":"string","minLength":1,"maxLength":256}},"statusFilter":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/BuildStatus"}},"branding":{"type":"object","properties":{"logoUrl":{"type":"string","format":"uri","description":"HTTPS logo shown in card headers. Pro plan."}}},"incidentMode":{"type":"boolean","description":"Collapse repeated failures into an updating card for bot-channel destinations. Pro plan."},"quietHours":{"type":"object","required":["start","end"],"description":"Pro plan.","properties":{"start":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"22:00"},"end":{"type":"string","pattern":"^\\d{2}:\\d{2}$","example":"07:00"},"tz":{"type":"string","maxLength":64,"default":"UTC","example":"America/Los_Angeles"}}},"dedupeWindowSec":{"type":"integer","minimum":0,"maximum":86400,"description":"Pro plan when > 0."},"failureAfterSuccessOnly":{"type":"boolean","description":"Pro plan."},"mentionsOnFailure":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Mention"},"description":"Pro plan."},"routes":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/Route"}},"mapping":{"type":"object","description":"Field mapping used by generic-platform subscriptions (dot-paths into the payload).","properties":{"title":{"type":"string","maxLength":256},"status":{"type":"string","maxLength":256},"link":{"type":"string","maxLength":256},"summary":{"type":"string","maxLength":256},"facts":{"type":"array","maxItems":20,"items":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","minLength":1,"maxLength":80},"value":{"type":"string","minLength":1,"maxLength":256}}}},"statusMap":{"type":"object","additionalProperties":{"type":"string","enum":["success","failure","running","neutral"]}}}},"enabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"ingestUrl":{"type":"string","description":"Per-subscription URL to give your pipeline. Includes the API key as a query param only on POST /subscriptions and POST /subscriptions/{id}/rotate-key."},"apiKey":{"type":"string","description":"Plaintext API key — only returned by POST /subscriptions and rotate-key. Never stored."},"lastDelivery":{"$ref":"#/components/schemas/DeliveryHealth","description":"Only on GET /subscriptions (list); absent when no delivery has been attempted."}}},"DeliveryHealth":{"type":"object","required":["ok","at"],"properties":{"ok":{"type":"boolean"},"at":{"type":"string","format":"date-time"},"error":{"type":"string","maxLength":300},"eventStatus":{"type":"string"},"pipeline":{"type":"string","maxLength":200}}},"AuditEntry":{"type":"object","required":["id","orgId","resource","resourceId","action","by","at"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string"},"resource":{"type":"string","enum":["subscription","approval"]},"resourceId":{"type":"string"},"action":{"type":"string","enum":["create","update","delete","decide"]},"by":{"type":"string"},"at":{"type":"string","format":"date-time"},"before":{},"after":{}}},"AuditPage":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/AuditEntry"}},"continuation":{"type":"string","description":"Present when another page exists."}}},"ApprovalInput":{"type":"object","required":["subscriptionId","pipeline","environment"],"properties":{"subscriptionId":{"type":"string","minLength":1,"maxLength":256},"pipeline":{"type":"string","minLength":1,"maxLength":256},"environment":{"type":"string","minLength":1,"maxLength":256},"runUrl":{"type":"string","format":"uri","description":"HTTPS only."},"ttlSeconds":{"type":"integer","minimum":1,"maximum":604800,"default":86400},"approvers":{"type":"array","maxItems":20,"description":"Optional approver allow-list, enforced only for bot-delivered approvals (the card's buttons round-trip through /api/messages with a verified Entra identity). Non-empty: only these users may decide from the card. Absent/empty: any channel member may. Ignored for webhook-delivered approvals, which cannot verify identity.","items":{"$ref":"#/components/schemas/Approver"}}}},"Approver":{"type":"object","required":["aadObjectId"],"properties":{"aadObjectId":{"type":"string","minLength":1,"maxLength":64,"description":"Entra object id of the user."},"name":{"type":"string","maxLength":128,"description":"Display name shown on the card."}}},"ApprovalCreated":{"type":"object","required":["id","orgId","expiresAt","approveUrl","rejectUrl"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"approveUrl":{"type":"string","format":"uri","description":"Bearer link — carries the approval token. Returned to the API caller for both delivery kinds, but only placed in the Teams card for webhook (Workflows) deliveries; bot-delivered cards carry no token and decide via an Action.Execute invoke instead."},"rejectUrl":{"type":"string","format":"uri","description":"Bearer link — carries the approval token (see approveUrl)."}}},"Approval":{"type":"object","description":"An approval as listed. The `token` is never returned.","required":["id","orgId","subscriptionId","pipeline","environment","createdAt","expiresAt"],"properties":{"id":{"type":"string","format":"uuid"},"orgId":{"type":"string"},"subscriptionId":{"type":"string"},"pipeline":{"type":"string"},"environment":{"type":"string"},"runUrl":{"type":"string","format":"uri"},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"approvers":{"type":"array","items":{"$ref":"#/components/schemas/Approver"}},"decision":{"type":"string","enum":["approved","rejected"]},"decidedAt":{"type":"string","format":"date-time"},"decidedBy":{"type":"string","description":"Bot decisions: `<display name> (aad:<object id>)` from the verified Teams identity. Link decisions: `anonymous`."},"decidedVia":{"type":"string","enum":["bot","link"],"description":"`bot` = Approve/Reject clicked on a bot-delivered card (Action.Execute invoke through /api/messages, verified identity, approver allow-list enforced). `link` = the token approve/reject URL."}}},"ApprovalPage":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Approval"}},"continuation":{"type":"string"}}},"BotChannelRecord":{"type":"object","required":["id","orgId","conversationId","serviceUrl","installedAt"],"properties":{"id":{"type":"string"},"orgId":{"type":"string"},"conversationId":{"type":"string"},"serviceUrl":{"type":"string","format":"uri"},"displayName":{"type":"string"},"tenantId":{"type":"string"},"installedAt":{"type":"string","format":"date-time"},"installedBy":{"type":"string"}}},"BotUserRecord":{"type":"object","required":["aadObjectId","name"],"properties":{"aadObjectId":{"type":"string"},"name":{"type":"string"}}},"BillingPlan":{"type":"object","required":["plan","hasStripeCustomer"],"properties":{"plan":{"type":"string","enum":["free","pro"]},"planUpdatedAt":{"type":"string","format":"date-time"},"hasStripeCustomer":{"type":"boolean"}}},"RedirectUrl":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}},"CardFinding":{"type":"object","required":["level","code","message"],"properties":{"level":{"type":"string","enum":["error","warning","info"]},"code":{"type":"string"},"message":{"type":"string"}}},"PreviewResult":{"type":"object","required":["ok","valid","summary","card","findings","stats"],"properties":{"ok":{"type":"boolean"},"valid":{"type":"boolean"},"summary":{"type":"string"},"card":{"type":["object","null"],"description":"The Adaptive Card that would be posted, or null when the payload is not a MessageCard."},"findings":{"type":"array","items":{"$ref":"#/components/schemas/CardFinding"}},"stats":{"type":"object","properties":{"sections":{"type":"integer"},"facts":{"type":"integer"},"images":{"type":"integer"},"actions":{"type":"integer"}}}}},"UiConfig":{"type":"object","required":["entraClientId","apiScope","loginScopes","authority","billingEnabled","freeSubscriptionLimit"],"properties":{"entraClientId":{"type":"string"},"apiScope":{"type":"string"},"loginScopes":{"type":"array","items":{"type":"string"}},"authority":{"type":"string","format":"uri"},"billingEnabled":{"type":"boolean"},"freeSubscriptionLimit":{"type":"integer"}}},"SsoDiagnostics":{"type":"object","required":["ok","expected","checks","claims"],"properties":{"ok":{"type":"boolean"},"expected":{"type":"object","properties":{"clientId":{"type":["string","null"]},"audienceForms":{"type":"array","items":{"type":"string"}},"requiredAppRole":{"type":["string","null"]}}},"checks":{"type":"array","items":{"type":"object","required":["id","label","status"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"status":{"type":"string","enum":["pass","fail","skip"]},"detail":{"type":"string"},"fix":{"type":"string"}}}},"claims":{"type":["object","null"],"description":"Unverified decoded claims of the presented token (display only)."}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"details":{}}},"Delivered":{"type":"object","properties":{"delivered":{"type":"integer"},"failed":{"type":"integer"},"reason":{"type":"string","enum":["no_subscriptions","filtered","ping"]},"queued":{"type":"boolean","description":"DISPATCH_MODE=queue: accepted for async delivery."},"subscriptions":{"type":"integer"}}},"TestResult":{"type":"object","required":["ok","delivered","mode"],"properties":{"ok":{"type":"boolean"},"delivered":{"type":"boolean"},"mode":{"type":"string","enum":["bot","dm","webhook"]}}},"MonitoringStatus":{"type":"object","required":["ok","status","generatedAt","summary","subscriptions"],"properties":{"ok":{"type":"boolean"},"status":{"type":"string","enum":["healthy","degraded","unknown"]},"generatedAt":{"type":"string","format":"date-time"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"enabled":{"type":"integer"},"healthy":{"type":"integer"},"failing":{"type":"integer"},"unknown":{"type":"integer"},"disabled":{"type":"integer"}}},"subscriptions":{"type":"array","items":{"type":"object"}}}}}},"paths":{"/api/health":{"get":{"summary":"Liveness probe","security":[],"responses":{"200":{"description":"OK"}}}},"/api/ready":{"get":{"summary":"Dependency-aware readiness probe","security":[],"responses":{"200":{"description":"Ready"},"503":{"description":"Not ready"}}}},"/api/monitoring/status":{"get":{"summary":"Tenant-scoped subscription delivery status","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Operational snapshot without destination credentials or payloads","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MonitoringStatus"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions":{"get":{"summary":"List subscriptions for the org","description":"Each item carries `lastDelivery` (when a delivery has been attempted) and `hasTeamsWebhookUrl`; the webhook URL itself is never returned.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Subscriptions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Subscription"}}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a subscription","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionInput"}}}},"responses":{"201":{"description":"Created (response includes plaintext apiKey ONCE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"400":{"description":"Validation error (details carry the zod field errors)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Feature or quota requires the Pro plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"get":{"summary":"Read a subscription","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"OK","headers":{"ETag":{"description":"Current optimistic-concurrency token. Send this exact value in If-Match for PUT.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"summary":"Replace all configurable subscription fields","description":"Requires the exact ETag returned by GET. Omitted optional fields are cleared; server-owned identity and API-key fields are preserved. `platform` cannot change.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"parameters":[{"name":"If-Match","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionInput"}}}},"responses":{"200":{"description":"Replaced","headers":{"ETag":{"description":"Current optimistic-concurrency token. Send this exact value in If-Match for PUT.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"400":{"description":"Invalid full replacement, or platform change attempted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Feature or quota requires the Pro plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"412":{"description":"ETag is stale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"428":{"description":"Exact If-Match header required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a subscription","description":"Plan gating applies to the delta only: a Free tenant may edit unrelated fields but not turn on a Pro feature.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPatch"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"400":{"description":"Invalid patch, platform change attempted, or the merged record would not have exactly one destination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Feature or quota requires the Pro plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Concurrent update conflict (retry)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete a subscription","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions/{id}/rotate-key":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"Rotate the subscription API key","description":"Issues a new API key and replaces the stored hash. Returns the plaintext key once plus an updated ingestUrl.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Rotated (response includes plaintext apiKey ONCE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Concurrent update conflict (retry)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/subscriptions/{id}/test":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"post":{"summary":"Send a synthetic success card through the subscription's real delivery path","description":"Bypasses statusFilter / pipelineAllowList (the test is about connectivity). Records the outcome as the subscription's lastDelivery.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Delivered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestResult"}}}},"400":{"description":"Subscription disabled, or DM target has not added the app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Teams / Bot Framework / Stripe) call failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/webhooks/{platform}":{"parameters":[{"name":"platform","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Platform"}}],"post":{"summary":"Broadcast webhook (header-aware callers)","description":"Used by callers that can attach `x-org-id` and `x-api-key` headers. Fans out to every enabled subscription on the org+platform whose key matches. Platform signature preflights (GitHub/GitLab/Jenkins/CircleCI/Buildkite secrets) run before the body is parsed.","security":[{"functionKey":[],"orgId":[],"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{}}},"responses":{"200":{"description":"Delivered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"202":{"description":"Accepted but nothing to deliver (no subscriptions, all filtered, queued, or a GitHub ping)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"207":{"description":"Partial success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"400":{"description":"Body too large (1 MiB), not JSON, or not a recognizable payload for the platform","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key or failed platform signature check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket refills."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"No destination could be delivered to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}}}}},"/api/ingest/{orgId}/{subId}/{platform}":{"parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"subId","in":"path","required":true,"schema":{"type":"string"}},{"name":"platform","in":"path","required":true,"schema":{"$ref":"#/components/schemas/Platform"}},{"name":"key","in":"query","required":false,"schema":{"type":"string"},"description":"API key. Prefer `x-api-key` header when callers can set headers."}],"post":{"summary":"Direct ingest (single subscription)","description":"Designed for callers that cannot send custom headers (Jenkins, GitHub, GitLab native webhook UIs). The URL itself targets one subscription; the key is in the query string. The same per-platform signature preflights as /api/webhooks/{platform} run here. Every pre-authentication failure (unknown subscription, disabled, platform mismatch, bad key) is a single 401 so the route does not reveal which subscriptions exist. Free-text fields (pipeline, job, branch, author, run id) are clamped to 256 characters before rendering.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{}}},"responses":{"200":{"description":"Delivered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"202":{"description":"Accepted but nothing to deliver (filtered, queued, or a GitHub `ping`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"207":{"description":"Partial success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}},"400":{"description":"Body too large (1 MiB), not JSON, or not a recognizable payload for the platform","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized — missing/invalid key, unknown or disabled subscription, platform mismatch, or failed signature check","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket refills."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"No destination could be delivered to","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivered"}}}}}}},"/api/audit":{"get":{"summary":"List mutation events for the org (paged, newest first)","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"parameters":[{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Clamped to [1, 500]; non-numeric falls back to 100."},{"name":"continuation","in":"query","schema":{"type":"string"},"description":"Opaque token from the previous page. Omit for the first page."}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditPage"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/approvals":{"get":{"summary":"List deployment approvals for the org (paged, newest first)","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"parameters":[{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"description":"Clamped to [1, 500]; non-numeric falls back to 100."},{"name":"continuation","in":"query","schema":{"type":"string"},"description":"Opaque token from the previous page. Omit for the first page."}],"responses":{"200":{"description":"One page (approval tokens are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPage"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Create a deployment approval and post its card to the subscription's channel","description":"Pro plan. The subscription must have a teamsWebhookUrl or botChannel destination; dmUser subscriptions are not supported for approvals. orgId comes from the caller's identity, never the body. Delivery decides how the card's buttons work: botChannel → Action.Execute buttons with NO token in the card (decisions arrive as an `adaptiveCard/action` invoke on /api/messages, verb `approval/decide`, attributed to the verified Entra identity and checked against `approvers`); teamsWebhookUrl → Action.OpenUrl token links (anonymous). The approveUrl/rejectUrl in the response are returned in both cases for the API caller's own use.","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalInput"}}}},"responses":{"201":{"description":"Created and posted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalCreated"}}}},"400":{"description":"Invalid body, or subscription has no supported destination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Feature or quota requires the Pro plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Subscription not found or disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Teams / Bot Framework / Stripe) call failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/approvals/{orgId}/{id}/{action}":{"parameters":[{"name":"orgId","in":"path","required":true,"schema":{"type":"string"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["approve","reject"]}},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"Approval bearer token from the card / create response."}],"get":{"summary":"Confirmation page for an approve/reject link (never mutates)","description":"Renders an HTML page with a POST form. Link scanners and prefetchers issue GET, so a decision can only be recorded by the POST. Responses carry `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. Rate limited per approval.","security":[],"responses":{"200":{"description":"HTML confirmation page","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"Unknown action","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or wrong token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket refills."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Record the decision","description":"Idempotent on the same decision (no second audit row). The decider is recorded as `anonymous` with `decidedVia: link` — the link is a bearer capability, not an identity. Verified approver identity is only available on the bot path (Approve/Reject on a bot-delivered card).","security":[],"responses":{"200":{"description":"HTML result page","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"Unknown action","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing or wrong token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided the other way, or concurrent decision conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"Approval expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket refills."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/bot/channels":{"get":{"summary":"Teams channels the bot is installed in for the caller's tenant","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Newest first (at most 500)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotChannelRecord"}}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/bot/users":{"get":{"summary":"Users who have opened a 1:1 chat with the bot (DM destination candidates)","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Display fields only (at most 500)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BotUserRecord"}}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/billing/plan":{"get":{"summary":"Current billing plan for the caller's tenant","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BillingPlan"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No installation record for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/billing/checkout":{"post":{"summary":"Create a Stripe Checkout session for the Pro plan","description":"Token-authed callers only (a host-key caller has no tenant). Returns the Checkout URL to redirect the browser to.","security":[{"entraBearer":[]}],"responses":{"200":{"description":"Checkout URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectUrl"}}}},"400":{"description":"PUBLIC_BASE_URL or STRIPE_PRICE_ID not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not a token-authed caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Tenant already has an active Pro subscription; use the billing portal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Teams / Bot Framework / Stripe) call failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/billing/portal":{"post":{"summary":"Create a Stripe customer-portal session","security":[{"entraBearer":[]},{"functionKey":[],"orgId":[]}],"responses":{"200":{"description":"Portal URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RedirectUrl"}}}},"400":{"description":"Tenant has no Stripe customer yet, or PUBLIC_BASE_URL not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Authentication failed (missing/invalid token or key, tenant not installed, or missing admin role)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No installation record for this tenant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"Upstream (Teams / Bot Framework / Stripe) call failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/preview":{"post":{"summary":"Migration Assistant: lint a legacy MessageCard and preview its Adaptive Card","description":"Anonymous and CORS-open. Body capped at 256 KiB. An unrecognizable card is a 200 with `valid: false` and findings, not a 400.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A legacy Office 365 MessageCard."}}}},"responses":{"200":{"description":"Lint result + preview card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewResult"}}}},"400":{"description":"Empty body, payload too large, or not JSON","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds until the bucket refills."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"options":{"summary":"CORS preflight","security":[],"responses":{"204":{"description":"CORS headers"}}}},"/api/ui/config":{"get":{"summary":"Public bootstrap config for the admin SPA","security":[],"responses":{"200":{"description":"Config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UiConfig"}}}}}}},"/api/ui/diag":{"get":{"summary":"SSO self-diagnosis for the presented Bearer token","description":"Anonymous: inspects only the caller's own token and reports which management-plane gate would fail. Failure details are a fixed vocabulary, not raw validator messages.","security":[],"parameters":[{"name":"Authorization","in":"header","required":false,"schema":{"type":"string"},"description":"Bearer <token> to diagnose."}],"responses":{"200":{"description":"Diagnostics","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SsoDiagnostics"}}}}}}},"/api/messages":{"post":{"summary":"Bot Framework messaging endpoint","description":"Receives Teams activities (conversationUpdate, message, invoke for the compose extension). Not a public API — the Bot Framework service is the only intended caller.","security":[{"botFrameworkJwt":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Bot Framework Activity."}}}},"responses":{"200":{"description":"Activity handled (body depends on activity type)"},"401":{"description":"Missing or invalid Bot Framework token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/stripe/webhook":{"post":{"summary":"Stripe webhook receiver for plan changes","description":"Verifies Stripe-Signature over the raw body. Handles checkout.session.completed (only when payment_status is paid), checkout.session.async_payment_succeeded / _failed, customer.subscription.updated / deleted, invoice.payment_failed. Test-mode events are rejected in production. Always 200 for unhandled or ignored events so Stripe does not retry.","security":[{"stripeSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Stripe Event."}}}},"responses":{"200":{"description":"Applied or deliberately ignored (`ignored: true, reason`)"},"400":{"description":"Missing/invalid signature, secret not configured, test-mode event in production, or missing metadata.tenantId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal error (details are never returned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/install/start":{"get":{"summary":"Begin Entra admin-consent install flow","security":[],"responses":{"302":{"description":"Redirect to Microsoft admin-consent endpoint"},"400":{"description":"ENTRA_CLIENT_ID or PUBLIC_BASE_URL not configured"}}}},"/api/install/callback":{"get":{"summary":"Entra admin-consent callback","security":[],"parameters":[{"name":"tenant","in":"query","schema":{"type":"string"}},{"name":"admin_consent","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string"}},{"name":"error","in":"query","schema":{"type":"string"}},{"name":"error_description","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Installation recorded (HTML page)"},"400":{"description":"Consent error or missing fields (HTML page)"}}}},"/api/openapi.json":{"get":{"summary":"This document","security":[],"responses":{"200":{"description":"OK"}}}}}}