{"openapi":"3.1.0","info":{"title":"XRAY Platform API","version":"1.0.0","description":"The complete integration surface of the XRAY platform: the endpoints a\nmicroservice calls on **XRAY Workspace** (signed events, subject\ncontext, service data plane), the **uniform service contract** every\nfirst-party microservice implements, and each service's own machine API\n(**Schema Analysis**, **Error Monitoring**, **Decisions**, **Voice to\nAction**, **Expert Sessions**).\n\n### Envelope\nSuccess responses are `{ \"data\": ... }`; failures are\n`{ \"error\": { \"code\", \"message\", \"details?\" } }` (see `ErrorEnvelope`\nand the `x-error-codes` taxonomy). A handful of legacy Blob routes still\nreturn `{ \"error\": \"message\" }` — marked `x-legacy-envelope: true`.\n\n### Auth planes\n* **AUTH-1** — static per-service transport key header\n  (`X-Blob-Api-Key`, `X-Guardian-Api-Key`, `X-Decide-Api-Key`,\n  `x-dictate-api-key`). Fail-closed: production answers 503 when unset.\n* **AUTH-2** — HMAC event signature on the workspace event bus.\n* **AUTH-4** — `X-Workspace-Token`: HS256 JWT minted by the workspace\n  (iss `workspace.xray.tech`, aud = the service id, exp−iat ≤ 300s).\n* **AUTH-5** — cron secrets for scheduled internal jobs.\n* **AUTH-6** — HMAC context signature for workspace pull APIs.\nAdditional planes: Decisions caller keys (`Bearer dcd_…`), Error\nMonitoring public consumer keys (`Bearer xrg_live_…`), Error Monitoring\nadmin allowlist, Expert Sessions' internal shared key, provider-signed\nwebhooks.\n\nDownload: JSON at `/api/v1/openapi`, YAML at `/api/v1/openapi?format=yaml`.\n","contact":{"name":"XRAY Platform","url":"https://workspace.xray.tech/api"}},"servers":[{"url":"https://workspace.xray.tech","description":"XRAY Workspace (hub) — every path without a servers override"}],"tags":[{"name":"workspace","description":"Service-facing endpoints the workspace exposes to microservices."},{"name":"service-contract","description":"The uniform /api/v1 plane every first-party microservice implements (health, ready, manifest, version, summary, activity, search, facts, pages, actions). Documented once; `x-service` lists the implementers and the path-level `servers` array carries each base URL. Voice to Action does not implement this plane yet."},{"name":"blob","description":"Schema Analysis (blob) — data-source schema indexing & insights (aud `blob`)."},{"name":"guardian","description":"Error Monitoring (guardian) — error & automation monitoring (aud `guardian`)."},{"name":"guardian-admin","description":"Error Monitoring (guardian) consumer-key administration (admin email allowlist)."},{"name":"guardian-public","description":"Error Monitoring (guardian) public read-only consumer API (Bearer xrg_live_…; default OFF)."},{"name":"decide-caller","description":"Decisions (decide) caller plane — external services request human decisions (Bearer dcd_…)."},{"name":"decide-workspace","description":"Decisions (decide) workspace plane — inbox, decisions, roles, keys (AUTH-1 + AUTH-4)."},{"name":"dictate","description":"Voice to Action (dictate) — voice sessions → extracted actions (AUTH-1 + user-email header)."},{"name":"forloco","description":"Expert Sessions (forloco) machine surface — embed SSO mint, roster, provider webhooks."},{"name":"internal-cron","description":"Scheduled internal jobs (AUTH-1/AUTH-5) — not for integrators."}],"x-error-codes":[{"code":"VALIDATION_ERROR","http":400,"meaning":"Body or query failed validation"},{"code":"INVALID_JSON","http":400,"meaning":"Request body isn't valid JSON"},{"code":"UNKNOWN_EVENT_TYPE","http":400,"meaning":"Event type isn't in the workspace catalog"},{"code":"UNAUTHORIZED","http":401,"meaning":"Missing/invalid credential"},{"code":"FORBIDDEN","http":403,"meaning":"Authenticated but not entitled"},{"code":"EMBED_NOT_CONSENTED","http":403,"meaning":"The organization did not consent to full-page embedding for this installed app"},{"code":"NOT_FOUND","http":404,"meaning":"Missing or not owned — never reveal which"},{"code":"RATE_LIMITED","http":429,"meaning":"Throttled; retry with backoff"},{"code":"INTERNAL_ERROR","http":500,"meaning":"Unexpected; message stays user-safe"},{"code":"NOT_CONFIGURED","http":501,"meaning":"Service env not set — degrade, don't crash (permanent, never retried)"},{"code":"UPSTREAM_ERROR","http":502,"meaning":"A dependency is down"},{"code":"UNAVAILABLE","http":503,"meaning":"A current-authorization dependency is unavailable; deny the operation"}],"components":{"securitySchemes":{"registryApiKey":{"type":"apiKey","in":"header","name":"X-Workspace-Api-Key","description":"A registered app's service API key, held only by its backend. Combined with the same app's AUTH-4 user token in the authorization request body; the API key alone never grants a user's permissions. This is not the first-party AUTH-6 context-signature plane."},"blobApiKey":{"type":"apiKey","in":"header","name":"X-Blob-Api-Key","description":"AUTH-1: Blob's static transport key. Fail-closed (503 in prod when unset)."},"guardianApiKey":{"type":"apiKey","in":"header","name":"X-Guardian-Api-Key","description":"AUTH-1: Guardian's static transport key. Fail-closed."},"decideApiKey":{"type":"apiKey","in":"header","name":"X-Decide-Api-Key","description":"AUTH-1: Decide's static transport key (workspace plane + internal cron)."},"dictateApiKey":{"type":"apiKey","in":"header","name":"x-dictate-api-key","description":"AUTH-1: Dictate's static transport key. Fail-closed."},"workspaceToken":{"type":"apiKey","in":"header","name":"X-Workspace-Token","description":"AUTH-4 identity JWT minted by the workspace: HS256, iss `workspace.xray.tech`, aud = the target service id (`blob`, `guardian`, `decide`), subject email (+ `companyId` where the service is org-scoped), exp−iat ≤ 300s. Services verify with a constant-time signature check, reject `alg:none`/algorithm confusion, and never trust a forgeable `?email=` in its place."},"eventSignature":{"type":"apiKey","in":"header","name":"X-XR-Signature","description":"AUTH-2: v1=<hex HMAC-SHA256 of `<X-XR-Timestamp>.<raw body>`> with the producer's own signing key; timestamp within ±300s."},"contextSignature":{"type":"apiKey","in":"header","name":"X-XR-Signature","description":"AUTH-6: v1=<hex HMAC-SHA256 of `<X-XR-Timestamp>.<raw query string>`> with the service's own context key; the service names itself in X-Workspace-Service-Id."},"cronSecret":{"type":"apiKey","in":"header","name":"X-Cron-Secret","description":"AUTH-5: shared secret for scheduled internal jobs."},"decideCallerKey":{"type":"http","scheme":"bearer","bearerFormat":"dcd_…","description":"Decide-issued org-scoped caller API key (hashed at rest)."},"guardianPublicKey":{"type":"http","scheme":"bearer","bearerFormat":"xrg_live_…","description":"Guardian public consumer key, scoped to the key owner's org. The whole plane is OFF unless GUARDIAN_PUBLIC_API_ENABLED=true (404 when off)."},"guardianAdminEmail":{"type":"apiKey","in":"header","name":"x-guardian-admin-email","description":"Guardian admin allowlist (GUARDIAN_ADMIN_EMAILS) for consumer-key administration."},"dictateUserEmail":{"type":"apiKey","in":"header","name":"X-Dictate-User-Email","description":"Dictate's subject scoping — the acting user's email (header, `?email=` or JSON body). Dictate has no AUTH-4 JWT yet; combine with `dictateApiKey`."},"forlocoInternalAuth":{"type":"apiKey","in":"header","name":"internalauthorization","description":"Expert Sessions' internal shared key (HOURLY_INTERNAL_KEY twin), constant-time compared; the plane is feature-gated (404 when FEATURE_WORKSPACE_EMBED is off, 503 when the key is unset)."}},"schemas":{"RegistryAuthorizationRequest":{"type":"object","additionalProperties":false,"required":["token"],"properties":{"token":{"type":"string","minLength":1,"maxLength":4096,"description":"The original AUTH-4 token for this registered app. Identity is derived from its verified subject and companyId, never from caller-supplied email, organization or role fields. Never log it."}}},"RegistryAuthorization":{"type":"object","additionalProperties":false,"required":["active","subject","access","expiresAt","asOf"],"properties":{"active":{"const":true,"description":"A current authorization check succeeded; not a reusable session."},"subject":{"type":"object","additionalProperties":false,"required":["email","companyId"],"properties":{"email":{"type":"string","format":"email"},"companyId":{"type":"string"}}},"access":{"type":"object","additionalProperties":false,"required":["orgRole","deptSlugs","roleSlugs","permissions"],"properties":{"orgRole":{"type":"string"},"deptSlugs":{"type":"array","maxItems":8,"items":{"type":"string"}},"roleSlugs":{"type":"array","maxItems":8,"items":{"type":"string"}},"permissions":{"type":"array","maxItems":16,"items":{"type":"string"},"description":"Current, consented permission keys for this app's audience only."}}},"expiresAt":{"type":"string","format":"date-time","description":"The original token's expiry, never extended by this check."},"asOf":{"type":"string","format":"date-time","description":"Check time; not a positive-cache lifetime or a session expiry."}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"One of the x-error-codes taxonomy (service-specific codes allowed)."},"message":{"type":"string"},"details":{"description":"Optional structured validation detail (Blob/Decide)."}}}}},"InboundEvent":{"type":"object","required":["id","type","service","occurredAt","subject","data","contractVersion"],"properties":{"id":{"type":"string","description":"Producer ULID; the dedupe key."},"type":{"type":"string","description":"A type the catalog assigns to `service`."},"service":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"},"subject":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"companyId":{"type":"string"}}},"data":{"description":"Validated against the per-type schema (spec §7.3)."},"contractVersion":{"const":"v1"}}},"SubjectContextOrgUnitRef":{"type":"object","description":"A department or department-role reference (id + stable slug + display name).","required":["id","slug","name"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}},"SubjectContext":{"type":"object","required":["subject","permissions","preferences","asOf"],"properties":{"subject":{"type":"object","required":["email","tier"],"properties":{"email":{"type":"string","format":"email"},"companyId":{"type":"string"},"tier":{"type":"string","enum":["free","hourly","monthly"]}}},"permissions":{"type":"object","description":"Org role, workspace-admin flag, per-service access/maxScope."},"access":{"type":"object","description":"OMITTED unless the caller's field profile includes it (and the FEATURE_ACCESS_CLAIMS rollout flag is on). The subject's own access facts — full department/custom-role assignment set (slugs) and effective permission keys, tier-intersected and filtered to the calling service's namespace (mirrors the AUTH-4 `perms` claim). Advisory: services re-check, never widen.","required":["orgRole","departments","roles","permissions"],"properties":{"orgRole":{"type":"string","description":"Workspace org role (OWNER/ADMIN/MEMBER/VIEWER)."},"departments":{"type":"array","items":{"type":"string"}},"roles":{"type":"array","items":{"type":"string"}},"permissions":{"type":"array","items":{"type":"string"}}}},"toolBelt":{"type":"array","items":{"type":"string"},"description":"OMITTED unless the caller's field profile includes it."},"team":{"type":"object","description":"OMITTED unless the caller's profile includes it.","required":["organizationId","name","members"],"properties":{"organizationId":{"type":"string"},"name":{"type":"string"},"members":{"type":"array","items":{"type":"object","required":["email","name","role","department","departmentRole"],"properties":{"email":{"type":["string","null"]},"name":{"type":["string","null"]},"role":{"type":"string","description":"Workspace org role (OWNER/ADMIN/MEMBER/VIEWER)."},"department":{"description":"Primary department (dept-roles plan §3 Phase B); null when unassigned.","oneOf":[{"$ref":"#/components/schemas/SubjectContextOrgUnitRef"},{"type":"null"}]},"departmentRole":{"description":"Role held inside `department`; never non-null while `department` is null.","oneOf":[{"$ref":"#/components/schemas/SubjectContextOrgUnitRef"},{"type":"null"}]}}}}}},"preferences":{"type":"object","properties":{"serviceVisible":{"type":"object"}}},"asOf":{"type":"string","format":"date-time"}}},"EventCatalogEntry":{"type":"object","required":["type","producer"],"properties":{"type":{"type":"string","description":"e.g. `guardian.ticket.detected`."},"producer":{"type":"string","description":"One of the registered producer service ids."},"isNotification":{"type":"boolean"}}},"ServiceManifestFirstParty":{"type":"object","description":"The manifest shape first-party services (blob, guardian, decide) serve today and the workspace's manifest cache consumes (src/lib/workspace/service-manifest.ts).","required":["service","displayName","contractVersion","requiredTier"],"properties":{"service":{"type":"string"},"displayName":{"type":"string"},"tagline":{"type":"string","maxLength":140},"contractVersion":{"const":"v1"},"requiredTier":{"type":"string","enum":["free","hourly","monthly"]},"addOn":{"type":"boolean"},"workspaceRoute":{"type":"string","description":"Must start with `/`."},"capabilities":{"type":"object","properties":{"summary":{"type":"boolean"},"activity":{"type":"boolean"},"search":{"type":"boolean"},"notifications":{"type":"boolean"},"deepLinks":{"type":"boolean"},"pages":{"type":"boolean"},"embed":{"type":["object","null"],"properties":{"url":{"type":"string"}}}}},"emittedEvents":{"type":"array","items":{"type":"string"}},"consumesFacts":{"type":"array","items":{"type":"string"}},"webhook":{"type":"object","properties":{"retrySchedule":{"type":"array","items":{"type":"integer"},"description":"Seconds between redelivery attempts."}}}}},"ServiceManifestSelfServe":{"type":"object","description":"The manifest shape SELF-SERVE registered services submit (src/lib/platform/manifest-schema.ts) — author-controlled REQUESTS the platform validates and admins grant. NOTE — the first-party and self-serve manifest schemas have not been reconciled yet; a service's GET /api/v1/manifest answers with one of the two shapes.","required":["service","displayName","contractVersion","capabilities","certification"],"properties":{"service":{"type":"string","description":"Must equal the registered slug (TRUST-1 anti-spoofing)."},"displayName":{"type":"string"},"contractVersion":{"const":"v1"},"tagline":{"type":"string"},"description":{"type":"string"},"requiredTier":{"type":"string","enum":["free","hourly","monthly"]},"capabilities":{"type":"object","properties":{"pages":{"type":"boolean"},"events":{"type":"boolean"},"ai":{"type":"object","properties":{"paths":{"type":"array","items":{"type":"string"}}}}}},"requestedGrants":{"type":"object","properties":{"identity":{"type":"object","properties":{"companyId":{"type":"boolean"},"tier":{"type":"boolean"},"tools":{"type":"boolean"}}},"facts":{"type":"array","items":{"type":"string"}},"events":{"type":"array","items":{"type":"string"}},"embed":{"type":"boolean"},"byokRelayPaths":{"type":"array","items":{"type":"string"}}}},"certification":{"type":"object","required":["resourcePath","healthPath"],"properties":{"resourcePath":{"type":"string"},"recordPathTemplate":{"type":"string","description":"Contains `{id}`."},"healthPath":{"type":"string"}}}}},"HealthStatus":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["ok"]}}}}},"VersionInfo":{"type":"object","properties":{"data":{"type":"object","properties":{"service":{"type":"string"},"contractVersion":{"type":"string"},"build":{"type":"string"}}}}},"PageDocument":{"type":"object","description":"Server-driven UI page (BLOCK-CONTRACT.md; executable truth src/lib/platform/sdui/block-schema.ts). Summary depth here — blocks are a discriminated union on `type`.","required":["id","title","blocks"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"blocks":{"type":"array","items":{"$ref":"#/components/schemas/SduiBlock"}}}},"SduiBlock":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["heading","text","stat","list","table","form","actionRow","callout"],"description":"heading/text — copy; stat — labeled figure; list/table — rows; form — inputs posting to an actionId; actionRow — buttons invoking POST /api/v1/actions/{actionId}; callout — highlighted notice."}},"additionalProperties":true},"Ticket":{"type":"object","description":"Guardian error ticket (core fields; org-scoped).","required":["id","status"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["open","acknowledged","resolved"]},"summary":{"type":"string"},"automationId":{"type":"string"},"groupId":{"type":"string"},"assignee":{"type":"string"},"detectedAt":{"type":"string","format":"date-time"}}},"DecisionStatus":{"type":"string","enum":["pending","needs_info","resolved","expired","cancelled"]},"Urgency":{"type":"string","enum":["normal","urgent"]},"ResolutionKind":{"type":"string","enum":["chosen","declined","expired","expired_default"]},"DecisionOptionCreate":{"type":"object","required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":1000},"consequence":{"type":"string","maxLength":1000,"description":"e.g. 'Choosing this will…'"},"default":{"type":"boolean","description":"At most one option; requires expiresAt on the decision."}}},"DecisionOption":{"allOf":[{"$ref":"#/components/schemas/DecisionOptionCreate"},{"type":"object","required":["id"],"properties":{"id":{"type":"string"}}}]},"Fact":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","maxLength":80},"value":{"type":"string","maxLength":400}}},"Link":{"type":"object","required":["label","url"],"properties":{"label":{"type":"string","maxLength":80},"url":{"type":"string","format":"uri","description":"https only"}}},"DecisionCreate":{"type":"object","required":["title","roleSlug","options"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"roleSlug":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50},"body":{"type":"string","maxLength":32768,"description":"Markdown (rendered sanitized; raw HTML never rendered)"},"facts":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/Fact"}},"links":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Link"}},"options":{"type":"array","minItems":2,"maxItems":5,"items":{"$ref":"#/components/schemas/DecisionOptionCreate"}},"urgency":{"$ref":"#/components/schemas/Urgency"},"expiresAt":{"type":"string","format":"date-time"}}},"InfoRequest":{"type":"object","required":["id","question","askedBy","askedAt"],"properties":{"id":{"type":"string"},"question":{"type":"string","maxLength":2000},"askedBy":{"type":"string","format":"email"},"askedAt":{"type":"string","format":"date-time"},"answer":{"type":"string","maxLength":8192},"answeredAt":{"type":"string","format":"date-time"}}},"Resolution":{"type":"object","required":["kind"],"properties":{"kind":{"$ref":"#/components/schemas/ResolutionKind"},"optionId":{"type":"string"},"rationale":{"type":"string","maxLength":2000},"decidedBy":{"type":"string","description":"email, or system:expiry"},"decidedAt":{"type":"string","format":"date-time"}}},"AssignmentChange":{"type":"object","required":["actor","at"],"properties":{"fromRole":{"type":"string"},"toRole":{"type":"string"},"toEmail":{"type":"string","format":"email"},"actor":{"type":"string"},"at":{"type":"string","format":"date-time"},"note":{"type":"string","maxLength":500}}},"Decision":{"type":"object","required":["id","organizationId","callerName","roleSlug","status","title","options","urgency","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"ULID"},"organizationId":{"type":"string"},"callerName":{"type":"string"},"roleSlug":{"type":"string"},"status":{"$ref":"#/components/schemas/DecisionStatus"},"title":{"type":"string"},"body":{"type":"string"},"facts":{"type":"array","items":{"$ref":"#/components/schemas/Fact"}},"links":{"type":"array","items":{"$ref":"#/components/schemas/Link"}},"options":{"type":"array","items":{"$ref":"#/components/schemas/DecisionOption"}},"urgency":{"$ref":"#/components/schemas/Urgency"},"expiresAt":{"type":"string","format":"date-time"},"defaultOptionId":{"type":"string"},"directAssigneeEmail":{"type":"string","format":"email"},"resolution":{"$ref":"#/components/schemas/Resolution"},"infoRequests":{"type":"array","items":{"$ref":"#/components/schemas/InfoRequest"}},"assignmentHistory":{"type":"array","items":{"$ref":"#/components/schemas/AssignmentChange"}},"routing":{"$ref":"#/components/schemas/DecisionRouting"},"actions":{"type":"array","description":"Present on the detail read and on choose/decline/route/retry responses.","items":{"$ref":"#/components/schemas/DecisionActionView"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"DecisionRouting":{"type":"object","description":"Who can act on a decision, computed live. Present on the detail read,\nthe unrouted list, the route response, and create responses (caller\nplane and template raise); omitted from inbox list items.\n","required":["assigneeCount","unrouted"],"properties":{"assigneeCount":{"type":"integer","minimum":0,"description":"Active role holders, +1 when a member is assigned directly."},"unrouted":{"type":"boolean","description":"Open, and nobody can act on it."},"reason":{"type":"string","enum":["no_members","role_archived","role_missing"]}}},"DecisionActionView":{"type":"object","description":"One template action on a decision. Field names only, never values.","required":["id","optionId","type","summary","status","attempts"],"properties":{"id":{"type":"string"},"optionId":{"type":"string"},"type":{"type":"string","enum":["airtable_update"]},"summary":{"type":"string"},"status":{"type":"string","enum":["armed","not_triggered","queued","waiting_for_setup","retrying","succeeded","dead_lettered"]},"attempts":{"type":"integer","minimum":0},"lastError":{"type":"string","maxLength":200},"triggeredAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"nextAttemptAt":{"type":"string","format":"date-time"}}},"Role":{"type":"object","required":["id","slug","name","createdAt"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"memberCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"archivedAt":{"type":"string","format":"date-time"}}},"RoleCreate":{"type":"object","required":["slug","name"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50},"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":400}}},"Assignment":{"type":"object","required":["memberEmail","addedBy","addedAt"],"properties":{"memberEmail":{"type":"string","format":"email"},"addedBy":{"type":"string","format":"email"},"addedAt":{"type":"string","format":"date-time"}}},"ApiKey":{"type":"object","required":["id","name","keyPrefix","createdBy","createdAt"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"keyPrefix":{"type":"string","description":"e.g. dcd_a1b2"},"createdBy":{"type":"string","format":"email"},"createdAt":{"type":"string","format":"date-time"},"lastUsedAt":{"type":"string","format":"date-time"},"revokedAt":{"type":"string","format":"date-time"}}},"DecisionEnvelope":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["decision"],"properties":{"decision":{"$ref":"#/components/schemas/Decision"}}}}},"DecisionListEnvelope":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["decisions"],"properties":{"decisions":{"type":"array","items":{"$ref":"#/components/schemas/Decision"}},"nextCursor":{"type":"string"}}}}},"RoleEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"role":{"$ref":"#/components/schemas/Role"}}}}},"RoleListEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"roles":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}}}}},"AssignmentListEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"assignments":{"type":"array","items":{"$ref":"#/components/schemas/Assignment"}}}}}},"KeyEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/ApiKey"}}}}},"KeyListEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}},"KeyCreatedEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"key":{"$ref":"#/components/schemas/ApiKey"},"plaintext":{"type":"string","description":"Full key value — returned ONLY in create/rotate responses."}}}}},"WebhookEndpoint":{"type":"object","required":["id","url","createdAt"],"properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"createdAt":{"type":"string","format":"date-time"},"disabledAt":{"type":"string","format":"date-time"}}},"WebhookCreatedEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret":{"type":"string","description":"Signing secret — returned ONCE."}}}}},"WebhookListEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}},"TemplateParams":{"type":"object","description":"The flat string map a template renders from. At most 20 entries; keys are identifier-like and values are bounded to the fact-value limit, because every param becomes a fact on the raised decision.","maxProperties":20,"propertyNames":{"pattern":"^[a-zA-Z][a-zA-Z0-9-]*$","maxLength":40},"additionalProperties":{"type":"string","minLength":1,"maxLength":400}},"TemplateRaise":{"type":"object","required":["params"],"properties":{"params":{"$ref":"#/components/schemas/TemplateParams"}}},"TemplateActionConfig":{"type":"object","description":"The one v1 action type: Decide updates an Airtable record when the option carrying this config is chosen. `fields` values may embed `{{param}}` placeholders plus `{{resolvedBy}}` / `{{resolvedAt}}`.","required":["type","baseId","tableId","fields"],"properties":{"type":{"type":"string","enum":["airtable_update"]},"baseId":{"type":"string","pattern":"^app[a-zA-Z0-9]{14}$"},"tableId":{"type":"string","pattern":"^tbl[a-zA-Z0-9]{14}$"},"recordIdParam":{"type":"string","default":"recordId","maxLength":40,"pattern":"^[a-zA-Z][a-zA-Z0-9-]*$","description":"Names the raise param carrying the target record id."},"fields":{"type":"object","description":"1–20 field → value entries; values may embed placeholders.","minProperties":1,"maxProperties":20,"additionalProperties":{"type":"string","minLength":1,"maxLength":400}}}},"TemplateOptionCreate":{"type":"object","required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":120},"description":{"type":"string","maxLength":1000},"consequence":{"type":"string","maxLength":1000},"action":{"$ref":"#/components/schemas/TemplateActionConfig"}}},"TemplateOption":{"type":"object","required":["id","label","position"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"consequence":{"type":"string"},"position":{"type":"integer"},"action":{"$ref":"#/components/schemas/TemplateActionConfig"}}},"Template":{"type":"object","required":["id","slug","name","roleSlug","titleTemplate","urgency","options","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"roleSlug":{"type":"string"},"titleTemplate":{"type":"string"},"bodyTemplate":{"type":"string"},"urgency":{"$ref":"#/components/schemas/Urgency"},"options":{"type":"array","items":{"$ref":"#/components/schemas/TemplateOption"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"archivedAt":{"type":"string","format":"date-time"}}},"TemplateCreate":{"type":"object","required":["slug","name","roleSlug","titleTemplate","options"],"properties":{"slug":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50,"description":"Kebab-case, unique per org, and immutable after creation."},"name":{"type":"string","minLength":1,"maxLength":80},"roleSlug":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50},"titleTemplate":{"type":"string","minLength":1,"maxLength":200,"description":"May embed `{{param}}` placeholders."},"bodyTemplate":{"type":"string","maxLength":32768},"urgency":{"$ref":"#/components/schemas/Urgency"},"options":{"type":"array","minItems":2,"maxItems":5,"items":{"$ref":"#/components/schemas/TemplateOptionCreate"}}}},"TemplateUpdate":{"type":"object","minProperties":1,"description":"At least one field. `slug` is immutable and therefore absent; `options` replace the list wholesale, because partial option edits invite position/action drift on a list of at most five rows.","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"roleSlug":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50},"titleTemplate":{"type":"string","minLength":1,"maxLength":200},"bodyTemplate":{"type":["string","null"],"maxLength":32768},"urgency":{"$ref":"#/components/schemas/Urgency"},"options":{"type":"array","minItems":2,"maxItems":5,"items":{"$ref":"#/components/schemas/TemplateOptionCreate"}}}},"TemplateEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"template":{"$ref":"#/components/schemas/Template"}}}}},"TemplateListEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/Template"}}}}}}},"parameters":{"XrTimestamp":{"name":"X-XR-Timestamp","in":"header","required":true,"schema":{"type":"string"},"description":"Unix-seconds timestamp bound into the HMAC; ±300s skew."},"WorkspaceServiceId":{"name":"X-Workspace-Service-Id","in":"header","required":true,"schema":{"type":"string","enum":["guardian","blob","tasks","debrief"]},"description":"The calling service names itself; paired with its context key."},"DecisionId":{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"ULID"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255}},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"}},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}},"responses":{"ErrorUnauthorized":{"description":"Missing/invalid credential (UNAUTHORIZED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ErrorNotFound":{"description":"Missing or not owned — never reveal which (NOT_FOUND).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ErrorRateLimited":{"description":"Throttled; retry with backoff (RATE_LIMITED).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ErrorNotConfigured":{"description":"Service env not set (NOT_CONFIGURED, permanent).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"Missing/invalid credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"Not found (or not owned by this org)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Conflict":{"description":"invalid_transition or idempotency_conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Rate limited (Retry-After set)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PayloadTooLarge":{"description":"Body exceeds bounds (e.g. markdown > 32KiB)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationOrUnknownRole":{"description":"validation_error, or unknown_role with `details.validRoles: string[]`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationOrUnknownTemplate":{"description":"validation_error, or unknown_template with\n`details.validTemplates: string[]`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"paths":{"/api/webhooks/events":{"post":{"operationId":"workspaceDeliverEvent","tags":["workspace"],"summary":"Deliver one signed event (at-least-once; dedupe on (service, id))","security":[{"eventSignature":[]}],"parameters":[{"$ref":"#/components/parameters/XrTimestamp"},{"name":"X-XR-Event-Id","in":"header","required":true,"schema":{"type":"string"}},{"name":"X-XR-Delivery","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundEvent"}}}},"responses":{"200":{"description":"Duplicate — already processed; stop retrying."},"202":{"description":"Accepted (handled after ack)."},"400":{"description":"Permanent (unknown type, invalid data) — alert, never retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Bad/stale signature."},"403":{"description":"FORBIDDEN — a registered app's own `<slug>.usage.recorded` names an organization with no active install of that app (never installed, or revoked). Permanent — alert, never retry. A usage event that names no organization at all is a 400.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"Body over 64KB."},"429":{"description":"Per-service quota; retry with backoff."}}}},"/api/v1/registry/{slug}/authorize":{"post":{"operationId":"workspaceAuthorizeRegistryApp","tags":["workspace"],"summary":"Recheck a registered app user's current tenant access","description":"Backend-only authorization callback for registry-backed apps. Requires both the service API key and the original app-audience AUTH-4 token. Rechecks current membership, organization, tier, installation, consent, service availability and certification. Requires identity.companyId and identity.access consent plus enabled access claims; experimental/demo identities cannot authorize real-data operations. Uninstall or re-consent invalidates tokens issued at or before the installation epoch; second-granular tokens minted in the same second may be denied and need to be freshly minted after the boundary. Receivers must deny sensitive operations on any failure and must not positively cache this result or extend the original token lifetime. This is a next-check guarantee, not an atomic transaction with the app's database. Body limit: 8 KiB. All responses use Cache-Control: no-store.","security":[{"registryApiKey":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{3,32}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegistryAuthorizationRequest"}}}},"responses":{"200":{"description":"Current consent-filtered access; the app still enforces its own operation policy.","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RegistryAuthorization"}}}}}},"400":{"description":"Invalid slug, JSON, body shape or size (VALIDATION_ERROR).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"403":{"description":"No current authorization, required consent or valid installation epoch (FORBIDDEN).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Developer Platform is disabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/ErrorRateLimited"},"503":{"description":"Authorization dependencies are unavailable; deny the app operation (UNAVAILABLE).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/subject-context":{"get":{"operationId":"workspaceGetSubjectContext","tags":["workspace"],"summary":"Pull one user's cross-service context (field-filtered per caller)","security":[{"contextSignature":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}},{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"}],"responses":{"200":{"description":"The caller's field-entitled view; cache on `asOf`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SubjectContext"}}}}}},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"404":{"description":"No workspace user with that email."},"429":{"description":"Per-service quota."},"501":{"description":"No context keys configured."}}}},"/api/v1/event-catalog":{"get":{"operationId":"workspaceGetEventCatalog","tags":["workspace"],"summary":"The event-type registry (public, schema-only)","security":[],"parameters":[{"name":"service","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Registered types and their owning producers.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"contractVersion":{"const":"v1"},"producers":{"type":"array","items":{"type":"string"}},"types":{"type":"array","items":{"$ref":"#/components/schemas/EventCatalogEntry"}}}}}}}}}}}},"/api/v1/service-data/{provider}":{"get":{"operationId":"workspaceGetServiceData","tags":["workspace"],"summary":"Cross-service data plane: read search/blob/guardian data through the workspace (AUTH-6 signed, per-caller provider profile, subject-entitlement gated)","security":[{"contextSignature":[]}],"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["search","blob","guardian"]}},{"name":"email","in":"query","required":false,"description":"Subject email — required for the user-scoped providers (blob, guardian).","schema":{"type":"string","format":"email"}},{"name":"q","in":"query","required":false,"description":"Catalog query (min 2 chars) — required for provider `search`.","schema":{"type":"string","minLength":2,"maxLength":200}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":25}},{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"}],"responses":{"200":{"description":"Provider payload: `search` → catalog results; `blob` → summary + schema-only data-source index; `guardian` → summary + org metrics."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"403":{"description":"Caller's provider profile excludes this provider, or the subject is not entitled to the product."},"404":{"description":"Unknown provider or no workspace user with that email."},"429":{"description":"Per-service quota."},"501":{"description":"No context keys configured."}}}},"/api/v1/service-data/recordings/grant":{"post":{"operationId":"workspaceGrantRecordingAccess","tags":["workspace"],"summary":"Grant or revoke a prospect's playback access to a Debrief recording, on behalf of the extracted Onboarding service (AUTH-2 inbound signature)","description":"The onboarding portal runs as its own service, but `RecordingInvite` and `RecordingShareSettings` belong to Debrief — so arming an invite, previously a local insert, is an authenticated call in. Since Debrief was itself extracted (`xray-inc/workspace-debrief`) and took those tables with it, the workspace now FORWARDS the write to Debrief while still verifying the caller and owning the audit record. The onboarding service's contract, key and service id are unchanged. Only `onboarding-portal` may call it; any other service id gets the same opaque 401 as a bad signature. Every action is idempotent, because the caller retries a grant whose response it never saw rather than shipping a portal whose recording will not play.","security":[{"contextSignature":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["recordingId","email","grantedByEmail"],"properties":{"recordingId":{"type":"string","minLength":1,"maxLength":64},"email":{"type":"string","format":"email","maxLength":254,"description":"The prospect the grant is issued to."},"grantedByEmail":{"type":"string","format":"email","maxLength":254,"description":"The operator who armed or revoked the invite. Recorded as the audit actor — attribution must survive the extraction."},"action":{"type":"string","enum":["grant","revoke"],"default":"grant"}}}}}},"responses":{"200":{"description":"Applied (or already in that state — the call is idempotent)."},"400":{"description":"Body is not JSON, or fails validation."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"429":{"description":"Per-caller rate limit."},"501":{"description":"No signing key configured — fails closed, accepting nothing."},"502":{"description":"Debrief rejected or was unreachable — the caller should retry."}}}},"/api/v1/service-data/viewer-link":{"post":{"operationId":"workspaceMintViewerLink","tags":["workspace"],"summary":"Provision an external recording viewer and return their sign-in link, for the extracted Debrief service (AUTH-2 inbound signature)","description":"Inviting someone to a recording doubles as a workspace signup (the growth loop). Provisioning the account and minting the session is IDENTITY work, and identity stayed in the workspace when Debrief was extracted — the workspace is the single place people log in. So Debrief asks for a URL and composes the recording-specific email itself, rather than becoming a second identity issuer. Only `debrief` may call it; any other service id gets the same opaque 401 as a bad signature. NOT idempotent in the usual sense: each call rotates the user's session token, because that is what \"send me a fresh link\" means.","security":[{"contextSignature":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","invitedByEmail","landingPath"],"properties":{"email":{"type":"string","format":"email","maxLength":254,"description":"The external viewer being provisioned."},"invitedByEmail":{"type":"string","format":"email","maxLength":254,"description":"The attendee who invited them. Recorded as the audit actor."},"landingPath":{"type":"string","minLength":1,"maxLength":512,"description":"Where the invitee lands after sign-in, e.g. `/recordings/p/<slug>`. Constrained to a ROOT-RELATIVE path: an unconstrained redirect target would be an open redirect with a live session token attached to it."}}}}}},"responses":{"200":{"description":"Provisioned; returns the absolute sign-in URL.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"created":{"type":"boolean","description":"Whether a NEW workspace account was minted."}}}}}}}},"400":{"description":"Body is not JSON, or fails validation."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"429":{"description":"Per-caller rate limit."},"501":{"description":"No signing key or user registry configured — fails closed."}}}},"/api/v1/service-data/short-link":{"post":{"operationId":"workspaceMintServiceShortLink","tags":["workspace"],"summary":"Brand a microservice's share URL as a go.xray.tech link (AUTH-2 inbound signature)","description":"The shortener is workspace-side and stays that way: the Kutt credential, the de-duplication table and — the part that matters — the closed allowlist deciding which URL shapes may ever be shortened all live here, and that allowlist is only a guarantee while every mint on the branded domain passes through it. So Debrief names a namespace and a slug, and the workspace composes the absolute URL itself; a caller can never propose the origin or the path shape. The signature is the ownership proof — `/recordings/p/<slug>` is Debrief's own namespace. Only `debrief` may call it; any other service id gets the same opaque 401 as a bad signature. NEVER fails for a cosmetic reason: an unconfigured, disabled or unreachable shortener answers 200 with the LONG url and `shortened: false`, so a share or an invite email can never break because of this surface.","security":[{"contextSignature":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","slug"],"properties":{"kind":{"type":"string","enum":["recording"],"description":"The namespace being branded. A closed set, not a path: this is what stops the route from minting branded links to anywhere the caller likes."},"slug":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,64}$","description":"The share slug, base64url. An ADDRESS, not a credential: the page behind it asks the visitor which email they were invited with and checks the answer server-side, which is why it needs no high-entropy short address. The TOKENISED spelling of the same page cannot be requested here — there is no room for a query string in this body, and the guard would refuse it if there were."}}}}}},"responses":{"200":{"description":"The URL to send. Branded when the shortener could do it, and the original long URL when it could not.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"shortened":{"type":"boolean","description":"Whether `url` is the branded form. False means the long URL came back unchanged."}}}}}}}},"400":{"description":"Body is not JSON, or fails validation."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"429":{"description":"Per-caller rate limit."},"501":{"description":"No signing key configured — fails closed, accepting nothing."}}}},"/api/v1/service-data/matched-clients":{"get":{"operationId":"workspaceGetMatchedClientsForEngineer","tags":["workspace"],"summary":"The Expert Sessions clients one engineer is actively matched to (AUTH-6 signed read)","description":"Meeting Recaps restricts a CLIENT call's recording to the people on the call, that client's matched engineers and platform admins (owner decision, 2026-09-14). Who is matched to whom is decided in Expert Sessions and read here from the Hourly roster, which only the workspace holds a key for, so Debrief asks this one narrow question instead of holding the roster. The answer is the same read, 60-second cache and fail-closed posture that gates connecting a Library board to a client (US-PIN-21): an unreachable or unconfigured Hourly service answers an empty list, which the caller reads as \"matched to nobody\". Never answers \"every client\" — the admin branch is pinned off. Only `debrief` may call it; any other service id gets the same opaque 401 as a bad signature.","security":[{"contextSignature":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"},{"name":"email","in":"query","required":true,"description":"The engineer's address. The signature covers the raw query string.","schema":{"type":"string","format":"email","maxLength":254}}],"responses":{"200":{"description":"The client addresses whose active-engineer list contains `email`. Empty for someone who is nobody's engineer, and when the roster is unreachable.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"clients":{"type":"array","items":{"type":"string","format":"email"}}}}}}}}},"400":{"description":"Missing or malformed `email`."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"429":{"description":"Per-caller rate limit."},"500":{"description":"Unexpected failure (no detail returned)."},"501":{"description":"No signing key configured — fails closed, accepting nothing."}}}},"/api/v1/service-data/org-roster":{"get":{"operationId":"workspaceGetOrgRoster","tags":["workspace"],"summary":"An organization's members and lifecycle state, for a service that reconciles its own copy (AUTH-6 signed read)","description":"The workspace sends no lifecycle events, so a service that copies an org's membership at first touch — the CRM writes an `OrgMember` row per caller — never learns that a member left, or that the org was archived, deleted or folded into another by a domain merge. This answers both per org: the members by email and role, and `status` with the surviving org id after a merge (from the `OrgDomainMerge` journal; undone merges do not count; merged beats archived beats active). Fails closed: no key or no org store is 501, never an empty roster, which the caller would read as \"everyone left\". Only `crm` may call it; any other service id gets the same opaque 401 as a bad signature. WORKSPACE-CRM US-CRM-29.","security":[{"contextSignature":[]}],"parameters":[{"$ref":"#/components/parameters/WorkspaceServiceId"},{"$ref":"#/components/parameters/XrTimestamp"},{"name":"orgId","in":"query","required":true,"description":"The workspace organization id. The signature covers the raw query string.","schema":{"type":"string","minLength":1,"maxLength":64}}],"responses":{"200":{"description":"The roster and lifecycle verdict. Never cached.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"org":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["active","archived","merged","deleted"]},"name":{"type":"string","nullable":true},"archivedAt":{"type":"string","format":"date-time","nullable":true},"mergedIntoOrgId":{"type":"string","nullable":true,"description":"The surviving org after a domain merge."}}},"members":{"type":"array","description":"Empty for a deleted org. Members without a verified email are omitted.","items":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","admin","member","viewer"]}}}}}}}}}}},"400":{"description":"Missing or malformed `orgId`."},"401":{"description":"Bad/stale signature or unknown service (identical — no oracle)."},"429":{"description":"Per-caller rate limit."},"500":{"description":"Unexpected failure (no detail returned)."},"501":{"description":"No signing key, or no organization store — fails closed."}}}},"/api/v1/openapi":{"get":{"operationId":"workspaceGetOpenApi","tags":["workspace"],"summary":"This document (JSON by default; `?format=yaml` or Accept application/yaml)","security":[],"parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","yaml"]}}],"responses":{"200":{"description":"The canonical platform OpenAPI document.","content":{"application/json":{},"application/yaml":{}}},"429":{"description":"Rate limited (60/min)."}}}},"/api/v1/health":{"x-service":["blob","guardian","decide","workspace"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}},{"url":"https://workspace.xray.tech","description":"XRAY Workspace (hub)"}],"get":{"operationId":"serviceHealth","tags":["service-contract"],"summary":"Liveness (unauthenticated)","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}}}}},"/api/v1/ready":{"x-service":["blob","guardian","decide","workspace"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}},{"url":"https://workspace.xray.tech","description":"XRAY Workspace (hub)"}],"get":{"operationId":"serviceReady","tags":["service-contract"],"summary":"Readiness — dependencies reachable (unauthenticated)","security":[],"responses":{"200":{"description":"Ready."},"503":{"description":"Not ready (envelope)."}}}},"/api/v1/manifest":{"x-service":["blob","guardian","decide","dictate"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}},{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"serviceManifest","tags":["service-contract"],"summary":"Capability manifest (AUTH-1 where the service holds a key; spec §4.3)","security":[{"blobApiKey":[]},{"guardianApiKey":[]},{"decideApiKey":[]},{}],"responses":{"200":{"description":"The service's manifest (one of the two platform shapes).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/ServiceManifestFirstParty"},{"$ref":"#/components/schemas/ServiceManifestSelfServe"}]}}}}}},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"503":{"description":"AUTH-1 key unset in production (fail-closed)."}}}},"/api/v1/version":{"x-service":["blob","guardian","decide"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceVersion","tags":["service-contract"],"summary":"Build/contract diagnostics","security":[],"responses":{"200":{"description":"Version info.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VersionInfo"}}}}}}},"/api/v1/summary":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceSummary","tags":["service-contract"],"summary":"Workspace summary card for the acting subject","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` summary payload."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/activity":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceActivity","tags":["service-contract"],"summary":"Recent activity feed for the acting subject","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` activity payload."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/search":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceSearch","tags":["service-contract"],"summary":"Federated search within the service, scoped to the acting subject","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` scored hits."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/facts":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"operationId":"serviceConsumeFacts","tags":["service-contract"],"summary":"Consume brokered workspace facts (e.g. user.tier.changed)","security":[{"eventSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fact"],"properties":{"fact":{"type":"string"},"subject":{"type":"object","properties":{"email":{"type":"string","format":"email"},"companyId":{"type":"string"}}},"data":{"type":"object"}}}}}},"responses":{"202":{"description":"Fact accepted."},"400":{"description":"Unknown fact (envelope)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/pages":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceListPages","tags":["service-contract"],"summary":"Server-driven UI — list available PageDocuments","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: PageDocument[] }` (see PageDocument)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/pages/{id}":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"operationId":"serviceGetPage","tags":["service-contract"],"summary":"Server-driven UI — one PageDocument","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The page.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PageDocument"}}}}}},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/actions/{actionId}":{"x-service":["blob","guardian"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}},{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}},{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"operationId":"serviceExecuteAction","tags":["service-contract"],"summary":"Execute a workspace action declared by an SDUI page","security":[{"blobApiKey":[],"workspaceToken":[]},{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"actionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Action input (validated per action)."}}}},"responses":{"200":{"description":"`{ data: … }` action result."},"400":{"description":"Validation failure (envelope)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/webhooks/tier-sync":{"post":{"operationId":"workspaceTierSync","tags":["workspace"],"summary":"Expert Sessions billing → workspace tier sync (signed; replay-guarded)","security":[{"eventSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","tier"],"properties":{"email":{"type":"string","format":"email"},"tier":{"type":"string","enum":["free","hourly","monthly"]}}}}}},"responses":{"200":{"description":"Tier updated (previous tier audited)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"description":"No registry user with that email."}}}},"/api/webhooks/role-sync":{"post":{"operationId":"workspaceRoleSync","tags":["workspace"],"summary":"Expert Sessions approval → workspace registry role sync (signed; replay-guarded)","description":"Called when an engineer application is APPROVED, so the workspace registry records that the person is now an engineer. The registry `Role Type` is the only thing that produces the `hourly_engineer` persona (Expert Sessions + Meeting Recaps); without this an approved engineer stays a free-tier customer, derives to `organic_lead`, and lands in the lead funnel instead of their portal. Expert Sessions' portal chooser deliberately does not write the role — choosing the engineer portal makes someone an applicant, not a hire — so approval is the only event that may promote. Shares the tier-sync auth plane: same producer, trust boundary and rotating key. Only the assignable registry roles are accepted; `admin` is refused by construction (ENT-IAM-12), since platform privilege is the env allowlist and never a webhook.","security":[{"eventSignature":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","registryRole"],"properties":{"email":{"type":"string","format":"email"},"registryRole":{"type":"string","enum":["customer","consultant","engineer"]},"id":{"type":"string","minLength":8,"maxLength":200},"source":{"type":"string","maxLength":120}}}}}},"responses":{"200":{"description":"Role updated (previous role audited), already that role (\"unchanged\"), or a replayed id (\"duplicate\")."},"400":{"description":"Malformed payload","or a signed call with no id.":null},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"403":{"description":"That role cannot be assigned."},"404":{"description":"No registry user with that email."},"413":{"description":"Request body exceeds the webhook ceiling."},"429":{"$ref":"#/components/responses/ErrorRateLimited"},"503":{"description":"Workspace user registry is not configured."}}}},"/api/webhooks/ai-enrichment":{"post":{"operationId":"workspaceAiEnrichmentCallback","tags":["workspace"],"summary":"Airtable AI enrichment result delivery (single-use token)","description":"Delivery endpoint for the Airtable AI enrichment loop. An automation in the dedicated AI-enrichment base finishes its AI step and posts the generated text here. Authenticated by a SINGLE-USE token the workspace planted on the Airtable record at create time (only its hash is stored), so no durable shared secret lives in Airtable script source. Delivery is idempotent: a replay after the first success changes nothing and returns 200. An unknown requestId and a bad token return the same generic 401.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requestId","token","output"],"properties":{"requestId":{"type":"string","minLength":8,"maxLength":200},"token":{"type":"string","minLength":16,"maxLength":256},"output":{"type":"string","minLength":1,"maxLength":100000},"kind":{"type":"string","maxLength":64},"generatedAt":{"type":"string","maxLength":64}}}}}},"responses":{"200":{"description":"Already delivered — idempotent no-op."},"202":{"description":"Generated text stored and the requester notified."},"400":{"description":"Malformed or invalid callback payload."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"description":"Enrichment is not enabled on this deploy."},"413":{"description":"Request body exceeds the callback ceiling."},"429":{"$ref":"#/components/responses/ErrorRateLimited"}}}},"/api/v1/insights":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListInsights","tags":["blob"],"summary":"Deterministic schema insights for the acting subject","security":[{"blobApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` findings (code-keyed, table/field-anchored)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/insights/chat":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobInsightsChat","tags":["blob"],"summary":"Schema-only AI chat about the subject's indexed schemas","security":[{"blobApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"},"threadId":{"type":"string"}}}}}},"responses":{"200":{"description":"`{ data: … }` assistant reply (consent-gated)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"501":{"$ref":"#/components/responses/ErrorNotConfigured"}}}},"/api/v1/connectors":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListConnectors","tags":["blob"],"summary":"List available data-source connectors","security":[{"blobApiKey":[]}],"responses":{"200":{"description":"`{ data: … }` connector registry (13 sources)."}}}},"/api/v1/connectors/{type}/schema":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobConnectorExtractSchema","tags":["blob"],"summary":"Extract a normalized, structure-only schema via one connector","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Connector-specific credentials/locator (never row data)."}}}},"responses":{"200":{"description":"`{ data: … }` normalized schema."},"400":{"description":"Validation failure (envelope)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/connectors/{type}/resources":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobConnectorListResources","tags":["blob"],"summary":"List the resources (bases/sheets/databases) a credential can see","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"`{ data: … }` resource list."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/connectors/{type}/save":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobConnectorSave","tags":["blob"],"summary":"Persist a connector extraction as an indexed schema","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"type","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"`{ data: … }` saved schema reference."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/connectors/requests":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobRequestConnector","tags":["blob"],"summary":"Request a connector Blob doesn't ship yet","security":[{"blobApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["connector"],"properties":{"connector":{"type":"string"},"notes":{"type":"string"}}}}}},"responses":{"201":{"description":"Request recorded."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/schemas":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListSchemas","tags":["blob"],"summary":"The subject's indexed schema library (all connectors)","security":[{"blobApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` schema index (structure-only)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/schemas/{id}":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobGetSchema","tags":["blob"],"summary":"One indexed schema","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` schema detail."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/schemas/{id}/versions":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListSchemaVersions","tags":["blob"],"summary":"Version history for one schema","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` versions."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/schemas/{id}/versions/{versionId}":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobGetSchemaVersion","tags":["blob"],"summary":"One schema version (diff source)","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` version payload."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/schemas/{id}/department-tags":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"put":{"operationId":"blobSetSchemaDepartmentTags","tags":["blob"],"summary":"Replace a data source's department-visibility tags","description":"Set/replace the department slugs that scope who may see this indexed\nsource; `[]` clears them and the source is org-wide again. The mutation\nis scoped through the subject's own base membership, so a cross-org id\nanswers 404, never 403. Where workspace subject-context is reachable,\nonly a mirrored org admin (ADMIN/OWNER or `workspaceAdmin`) may call it;\nan unreachable context does NOT lock the surface — standalone Blob\ndeploys have no departments at all, and tags are subtractive-only, so\nthis can never widen access.\n","x-legacy-envelope":true,"security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["departmentTags"],"properties":{"departmentTags":{"type":"array","maxItems":32,"description":"Lowercase kebab-case slugs; duplicates collapse, `[]` clears.","items":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"}}}}}}},"responses":{"200":{"description":"Tags replaced.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["id","departmentTags"],"properties":{"id":{"type":"string"},"departmentTags":{"type":"array","items":{"type":"string"}}}}}}}}},"400":{"description":"Invalid body, or no subject (neither AUTH-4 token nor `?email=`)."},"401":{"description":"Missing/invalid service key."},"403":{"description":"The subject is not an org admin."},"404":{"description":"Schema not found, or not the subject's — never distinguished."},"429":{"description":"Rate limited (Retry-After set)."},"503":{"description":"Blob service key unset (production fails closed)."}}}},"/api/v1/extractions":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListExtractions","tags":["blob"],"summary":"List extraction jobs","security":[{"blobApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` extractions."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}},"post":{"operationId":"blobCreateExtraction","tags":["blob"],"summary":"Start an extraction job","security":[{"blobApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tokenId"],"properties":{"tokenId":{"type":"string"},"baseId":{"type":"string"}}}}}},"responses":{"201":{"description":"`{ data: … }` extraction started."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/extractions/{id}":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobGetExtraction","tags":["blob"],"summary":"One extraction job","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` extraction detail."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tokens":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListTokens","tags":["blob"],"summary":"List the subject's data-source tokens","security":[{"blobApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` tokens (never plaintext secrets)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}},"post":{"operationId":"blobAddToken","tags":["blob"],"summary":"Add a data-source token (PAT)","security":[{"blobApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"label":{"type":"string"}}}}}},"responses":{"201":{"description":"`{ data: … }` token stored."},"400":{"description":"Validation failure (envelope)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/tokens/{id}":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"delete":{"operationId":"blobRemoveToken","tags":["blob"],"summary":"Remove a data-source token","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tokens/{id}/bases":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobListTokenBases","tags":["blob"],"summary":"Discover the bases/resources visible to one token","security":[{"blobApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` base list."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/share-access":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"get":{"operationId":"blobListShareAccess","tags":["blob"],"summary":"List share-access grants for the subject","security":[{"blobApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` grants."}}},"post":{"operationId":"blobCreateShareAccess","tags":["blob"],"summary":"Record a shared-base access grant (service-account invite flow)","security":[{"blobApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"201":{"description":"Grant recorded."}}}},"/api/v1/share-access/process-pending":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobProcessPendingShareAccess","tags":["blob"],"summary":"Process pending shared-base grants (cron/ops)","security":[{"blobApiKey":[]}],"responses":{"200":{"description":"`{ data: … }` processed count."}}}},"/api/v1/sync-shared-bases":{"x-service":["blob"],"servers":[{"url":"{blobBaseUrl}","description":"Blob","variables":{"blobBaseUrl":{"default":"https://xr-blob.up.railway.app"}}}],"post":{"operationId":"blobSyncSharedBases","tags":["blob","internal-cron"],"summary":"Trigger the shared-base sync sweep (Railway cron)","security":[{"cronSecret":[]},{"blobApiKey":[]}],"responses":{"200":{"description":"`{ data: … }` sync summary."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/dashboard":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianDashboard","tags":["guardian"],"summary":"Dashboard PageDocument for the acting subject's org","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: PageDocument }`."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/metrics":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianMetrics","tags":["guardian"],"summary":"Org error metrics","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` org metrics (org-scoped, never cross-tenant)."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/tickets":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListTickets","tags":["guardian"],"summary":"Error tickets for the subject's org","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"Ticket list.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Ticket"}}}}}}},"401":{"$ref":"#/components/responses/ErrorUnauthorized"}}}},"/api/v1/tickets/{id}":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianGetTicket","tags":["guardian"],"summary":"One ticket","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Ticket detail.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Ticket"}}}}}},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tickets/{id}/resolve":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianResolveTicket","tags":["guardian"],"summary":"Resolve a ticket","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: Ticket }` resolved."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tickets/{id}/acknowledge":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianAcknowledgeTicket","tags":["guardian"],"summary":"Acknowledge a ticket","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: Ticket }` acknowledged."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tickets/{id}/reassign":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianReassignTicket","tags":["guardian"],"summary":"Reassign a ticket to an org member","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["assignee"],"properties":{"assignee":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"`{ data: Ticket }` reassigned."},"400":{"description":"Unknown member (envelope)."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/tickets/{id}/notes":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListTicketNotes","tags":["guardian"],"summary":"Notes on a ticket","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` notes."}}},"post":{"operationId":"guardianAddTicketNote","tags":["guardian"],"summary":"Add a note to a ticket","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string"}}}}}},"responses":{"201":{"description":"Note added."}}}},"/api/v1/groups":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListGroups","tags":["guardian"],"summary":"Error groups (rollups)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` groups."}}},"post":{"operationId":"guardianCreateGroup","tags":["guardian"],"summary":"Create an error group","security":[{"guardianApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}}}},"responses":{"201":{"description":"Group created."}}}},"/api/v1/groups/{id}":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianGetGroup","tags":["guardian"],"summary":"One group","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` group detail."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/groups/{id}/automations":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListGroupAutomations","tags":["guardian"],"summary":"Automations in a group","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` automations."}}}},"/api/v1/automations":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListAutomations","tags":["guardian"],"summary":"Indexed automations for the org","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` automations."}}}},"/api/v1/automations/{id}":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianGetAutomation","tags":["guardian"],"summary":"One automation","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` automation detail."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}},"patch":{"operationId":"guardianUpdateAutomation","tags":["guardian"],"summary":"Update an automation (owner/group)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"owner":{"type":"string"},"groupId":{"type":"string"}}}}}},"responses":{"200":{"description":"`{ data: … }` updated."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/automations/{id}/snooze":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianSnoozeAutomation","tags":["guardian"],"summary":"Snooze an automation's alerts","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"until":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"Snoozed."}}}},"/api/v1/events":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListErrorEvents","tags":["guardian"],"summary":"Error event log (org-scoped)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` events."}}}},"/api/v1/ingest/error":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianIngestError","tags":["guardian"],"summary":"Route-A error ingestion (off by default — GUARDIAN_INGEST_ENABLED)","security":[{"guardianApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Provider error payload (normalized on ingest)."}}}},"responses":{"202":{"description":"Ingested."},"404":{"description":"Ingestion disabled."}}}},"/api/v1/members":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListMembers","tags":["guardian"],"summary":"Org member directory (assignment targets)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` members."}}}},"/api/v1/notifications/routes":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianListNotificationRoutes","tags":["guardian"],"summary":"Notification routes for the org","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: … }` routes."}}},"post":{"operationId":"guardianCreateNotificationRoute","tags":["guardian"],"summary":"Create a notification route (webhook/Slack/email)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","target"],"properties":{"kind":{"type":"string","enum":["webhook","slack","email","in-app"]},"target":{"type":"string"},"filters":{"type":"object"}}}}}},"responses":{"201":{"description":"Route created."}}}},"/api/v1/notifications/routes/{id}":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianGetNotificationRoute","tags":["guardian"],"summary":"One notification route","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` route."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}},"patch":{"operationId":"guardianUpdateNotificationRoute","tags":["guardian"],"summary":"Update a route","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Updated."}}},"delete":{"operationId":"guardianDeleteNotificationRoute","tags":["guardian"],"summary":"Delete a route","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted."}}}},"/api/v1/notifications/routes/{id}/test":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianTestNotificationRoute","tags":["guardian"],"summary":"Send a safe test notification through a route","security":[{"guardianApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Test dispatched."}}}},"/api/v1/notifications/slack/install-url":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianSlackInstallUrl","tags":["guardian"],"summary":"Mint a Slack OAuth install URL (state-signed)","security":[{"guardianApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"`{ data: { url } }`."},"501":{"$ref":"#/components/responses/ErrorNotConfigured"}}}},"/api/v1/notifications/slack/callback":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianSlackCallback","tags":["guardian"],"summary":"Slack OAuth callback (browser redirect target)","security":[],"parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string"}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back to the workspace."},"400":{"description":"Invalid state/code."}}}},"/api/admin/v1/keys":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianAdminListKeys","tags":["guardian-admin"],"summary":"List public-API consumer keys","security":[{"guardianAdminEmail":[]}],"responses":{"200":{"description":"`{ data: … }` keys (hashed at rest, prefixes only)."},"403":{"description":"Not on the admin allowlist."}}},"post":{"operationId":"guardianAdminCreateKey","tags":["guardian-admin"],"summary":"Create a consumer key (xrg_live_…)","security":[{"guardianAdminEmail":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ownerOrg"],"properties":{"ownerOrg":{"type":"string"},"label":{"type":"string"}}}}}},"responses":{"201":{"description":"`{ data: { key } }` — plaintext shown once."}}}},"/api/admin/v1/keys/{id}/revoke":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianAdminRevokeKey","tags":["guardian-admin"],"summary":"Revoke a consumer key","security":[{"guardianAdminEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked."}}}},"/api/admin/v1/keys/{id}/rotate":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"post":{"operationId":"guardianAdminRotateKey","tags":["guardian-admin"],"summary":"Rotate a consumer key","security":[{"guardianAdminEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: { key } }` — new plaintext shown once."}}}},"/api/public/v1/tickets":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianPublicListTickets","tags":["guardian-public"],"summary":"Read-only tickets for the key owner's org","security":[{"guardianPublicKey":[]}],"responses":{"200":{"description":"`{ data: Ticket[] }`."},"404":{"description":"Public API disabled (GUARDIAN_PUBLIC_API_ENABLED)."}}}},"/api/public/v1/groups":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianPublicListGroups","tags":["guardian-public"],"summary":"Read-only groups for the key owner's org","security":[{"guardianPublicKey":[]}],"responses":{"200":{"description":"`{ data: … }`."},"404":{"description":"Public API disabled."}}}},"/api/public/v1/automations":{"x-service":["guardian"],"servers":[{"url":"{guardianBaseUrl}","description":"Guardian","variables":{"guardianBaseUrl":{"default":"https://xr-guardian.up.railway.app"}}}],"get":{"operationId":"guardianPublicListAutomations","tags":["guardian-public"],"summary":"Read-only automations for the key owner's org","security":[{"guardianPublicKey":[]}],"responses":{"200":{"description":"`{ data: … }`."},"404":{"description":"Public API disabled."}}}},"/api/health":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"dictateHealth","tags":["dictate"],"summary":"Liveness (dictate's only health surface — not under /api/v1 yet)","security":[],"responses":{"200":{"description":"OK."}}}},"/api/v1/feed":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"dictateFeed","tags":["dictate"],"summary":"Recent dictation sessions for the acting user","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20}}],"responses":{"200":{"description":"`{ data: … }` sessions feed."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"503":{"description":"AUTH-1 key unset in production (fail-closed)."}}}},"/api/v1/sessions":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"dictateListSessions","tags":["dictate"],"summary":"List the acting user's sessions","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"responses":{"200":{"description":"`{ data: { sessions } }`."}}},"post":{"operationId":"dictateCreateSession","tags":["dictate"],"summary":"Create a dictation session","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"Text brain-dump (audio uploads go to /audio)."}}}}}},"responses":{"201":{"description":"`{ data: { session } }`."}}}},"/api/v1/sessions/{id}":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"dictateGetSession","tags":["dictate"],"summary":"One session with its extracted actions","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` session detail."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/sessions/{id}/audio":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateUploadAudio","tags":["dictate"],"summary":"Upload session audio (multipart; transcribed when OPENAI_API_KEY set)","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"audio":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"`{ data: … }` transcription (or text-fallback notice)."}}}},"/api/v1/sessions/{id}/extract":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateExtractActions","tags":["dictate"],"summary":"Extract structured actions from a session","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: { actions } }`."},"404":{"$ref":"#/components/responses/ErrorNotFound"}}}},"/api/v1/actions/{id}/approve":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateApproveAction","tags":["dictate"],"summary":"Approve an extracted action","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approved."}}}},"/api/v1/actions/{id}/execute":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateExecuteAction","tags":["dictate"],"summary":"Execute an approved action (via Zapier MCP when connected)","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"`{ data: … }` execution result."},"501":{"$ref":"#/components/responses/ErrorNotConfigured"}}}},"/api/v1/actions/{id}/assign":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateAssignAction","tags":["dictate"],"summary":"Assign an action to the tasks sink","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Assigned."}}}},"/api/v1/integrations/zapier":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"get":{"operationId":"dictateGetZapierIntegration","tags":["dictate"],"summary":"The acting user's Zapier MCP connection state","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"responses":{"200":{"description":"`{ data: … }` connection state (never token material)."}}},"post":{"operationId":"dictateConnectZapier","tags":["dictate"],"summary":"Start a Zapier MCP OAuth connection","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"responses":{"200":{"description":"`{ data: { url } }` OAuth start."}}},"delete":{"operationId":"dictateDisconnectZapier","tags":["dictate"],"summary":"Disconnect Zapier (tokens destroyed)","security":[{"dictateApiKey":[],"dictateUserEmail":[]}],"responses":{"200":{"description":"Disconnected."}}}},"/api/v1/integrations/zapier/callback":{"x-service":["dictate"],"servers":[{"url":"{dictateBaseUrl}","description":"Dictate","variables":{"dictateBaseUrl":{"default":"https://xr-dictate-api-production.up.railway.app"}}}],"post":{"operationId":"dictateZapierCallback","tags":["dictate"],"summary":"Zapier OAuth callback (token stored encrypted at rest)","security":[{"dictateApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Connected."}}}},"/api/internal/auth/sso":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoMintEmbedSso","tags":["forloco"],"summary":"Mint a single-use (≤60s) embed SSO token for the workspace iframe","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"X-XR-Environment","in":"header","required":true,"description":"ADR-048 — the caller's XRAY environment (XR_ENVIRONMENT). Hourly refuses a mint whose value is missing or is not its own environment, so a staging workspace cannot sign anyone into production Hourly even when the internal key is shared.","schema":{"type":"string","enum":["production","staging","development","test"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"Token minted.","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"embedUrl":{"type":"string"},"role":{"type":"string"}}}}}},"401":{"description":"Bad internal key, or (after the key checks out) a missing or other-environment X-XR-Environment — code ENVIRONMENT_MISMATCH."},"404":{"description":"Feature off (FEATURE_WORKSPACE_EMBED)."},"503":{"description":"Internal key or XR_ENVIRONMENT unset/invalid on Hourly."}}}},"/api/internal/roster":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"get":{"operationId":"forlocoRoster","tags":["forloco"],"summary":"Engineer + client roster for workspace bubble-up","security":[{"forlocoInternalAuth":[]}],"responses":{"200":{"description":"Roster.","content":{"application/json":{"schema":{"type":"object","properties":{"roster":{"type":"array","items":{"type":"object","required":["email","kind"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"},"kind":{"type":"string","enum":["engineer","client"]}}}}}}}}}}}},"/api/internal/finance/balance":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"get":{"operationId":"forlocoFinanceBalance","tags":["forloco"],"summary":"A customer's money state, for the workspace finance BFF","description":"`email` is the workspace SESSION email, forwarded by a BFF the browser\ncannot reach around — nothing client-supplied scopes this read. An\nunknown email and a known user without a billing account are the SAME\n404, so the response never confirms who is or is not a customer. The\nemail is PII and is never logged on either side.\n","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}}],"responses":{"200":{"description":"Balance. Money is integer minor units plus an explicit currency.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["creditsHours","usedHours","adjustmentsHours","bookableHours","hoursAccountHours","balanceCents","currency"],"properties":{"creditsHours":{"type":"number","description":"Hours bought, across every rail."},"usedHours":{"type":"number","description":"Hours consumed by delivered work."},"adjustmentsHours":{"type":"number","description":"Signed manual adjustments, in hours."},"bookableHours":{"type":"number","description":"What the booking gate will actually honour: active, non-expired package hours minus booked upcoming sessions. NOT interchangeable with hoursAccountHours."},"hoursAccountHours":{"type":"number","description":"Paid-for minus used — the accounting view, usually LARGER. Showing it where bookableHours belongs tells a customer they have hours the app will refuse to spend. Already net of forfeitedHours."},"forfeitedHours":{"type":"number","minimum":0,"description":"Hours lost to package expiry, already subtracted from hoursAccountHours, so hoursAccountHours = creditsHours − usedHours + adjustmentsHours − forfeitedHours. Only a package whose expiry falls at or after Hourly's HOURS_EXPIRY_EFFECTIVE_FROM forfeits; 0 while that is unset. OPTIONAL — an older Hourly omits it, which means 0."},"balanceCents":{"type":"integer","description":"Outstanding balance; positive = the customer owes us."},"currency":{"type":"string"}}}}}}}},"400":{"description":"VALIDATION_ERROR — `email` missing or malformed."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"description":"UNKNOWN_SUBJECT — no billing account for that subject; also NOT_FOUND when FEATURE_WORKSPACE_EMBED is off (the plane is indistinguishable from unshipped)."},"503":{"$ref":"#/components/responses/ErrorNotConfigured"}}}},"/api/internal/finance/transactions":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"get":{"operationId":"forlocoFinanceTransactions","tags":["forloco"],"summary":"A customer's purchase and usage history, newest first","description":"Same auth, subject scoping and PII rules as `/api/internal/finance/balance`.","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":200},"description":"Opaque cursor from a previous page's `nextCursor`."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"One page of ledger entries.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["entries","nextCursor"],"properties":{"entries":{"type":"array","items":{"type":"object","required":["id","kind","description","amountCents","currency","hours","occurredAt","receiptUrl"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["purchase","usage","refund","adjustment","payment","overage"]},"description":{"type":"string"},"amountCents":{"type":"integer","description":"Signed; positive = charged to the customer."},"currency":{"type":"string"},"hours":{"type":["number","null"]},"occurredAt":{"type":"string","format":"date-time"},"receiptUrl":{"type":["string","null"],"description":"Hosted receipt URL when the processor has one."}}}},"nextCursor":{"type":["string","null"],"description":"Cursor for the next page, or null when exhausted."}}}}}}}},"400":{"description":"VALIDATION_ERROR — bad query parameters or cursor."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"description":"UNKNOWN_SUBJECT, or NOT_FOUND when FEATURE_WORKSPACE_EMBED is off."},"503":{"$ref":"#/components/responses/ErrorNotConfigured"}}}},"/api/internal/finance/portal-session":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoFinancePortalSession","tags":["forloco"],"summary":"Mint a Stripe Billing Portal session for a customer","description":"THE RETURN URL IS THE ATTACK SURFACE, and it is why this is not\n`/balance` with a POST. A portal link is a credential-adjacent\ndestination a signed-in customer is about to be sent to, so `returnUrl`\nis compared ORIGIN-TO-ORIGIN (never `startsWith`) against the workspace\nembed-origin allowlist — the same list that decides who may frame the\napp. Without that, a leaked shared key would be enough to turn XRAY's\nown Stripe portal into an open redirect. The workspace computes\n`returnUrl` server-side and never accepts it from the browser; this\ncheck is the far side of that same rule.\n","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["returnUrl"],"properties":{"returnUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Where Stripe returns the customer; origin must be allowlisted."}}}}}},"responses":{"200":{"description":"Session minted (short-lived, single-use).","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}}}},"400":{"description":"VALIDATION_ERROR — bad `email`/`returnUrl`, or a non-allowlisted origin."},"401":{"$ref":"#/components/responses/ErrorUnauthorized"},"404":{"description":"UNKNOWN_SUBJECT, or NOT_FOUND when FEATURE_WORKSPACE_EMBED is off."},"503":{"description":"NOT_CONFIGURED — the internal key is unset, Stripe billing is off (`FEATURE_STRIPE_BILLING`), or billing is not configured."}}}},"/api/internal/users/provision":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoProvisionPortalUser","tags":["forloco"],"summary":"Create (or recognise) the account behind a workspace user's \"engineer or customer?\" answer","description":"The write-side sibling of the SSO mint, and the only internal route that\nWRITES — so it is the narrowest. The caller sends an email and a kind\nand CANNOT send a role, an approval status or a profile id: the service\ndecides all three. An engineer is provisioned as an APPLICANT with an\ninactive, non-public profile, because a shared key proves the request\ncame from the workspace, not that the person behind it is vetted. It\nnever switches an existing profile, never elevates, never demotes, and\nis idempotent — the same call twice provisions once. `kind` in the\nresponse is what the person IS now, which on a `conflict` is not what\nwas asked; the workspace corrects its own record from it rather than\nre-asking.\n","x-legacy-envelope":true,"security":[{"forlocoInternalAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","kind"],"properties":{"email":{"type":"string","format":"email"},"name":{"type":"string","maxLength":200},"kind":{"type":"string","enum":["engineer","customer"]},"allowSwitch":{"type":"boolean","description":"Platform-admin override only. ADDS the requested profile beside an existing one; never swaps or deletes. The self-service question never sends it."}}}}}},"responses":{"200":{"description":"Outcome. Bare object, NOT the `{ data }` envelope — the workspace's provision client reads `status`/`kind` off the top level.","content":{"application/json":{"schema":{"type":"object","required":["status","kind"],"properties":{"status":{"type":"string","enum":["created","existing","profile_added","conflict","switched"]},"kind":{"type":"string","enum":["engineer","customer"]}}}}}},"400":{"description":"Invalid request body."},"401":{"description":"Bad or missing shared key."},"404":{"description":"FEATURE_WORKSPACE_EMBED is off — indistinguishable from unshipped."},"503":{"description":"Internal key unset."}}}},"/api/internal/staff/sales":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"put":{"operationId":"forlocoSetSalesCapability","tags":["forloco"],"summary":"Grant or revoke the Sales capability for one person","description":"The workspace is where \"only a platform admin may grant Sales\" is\nenforced (US-AD-80, ADR-047); this call carries the grant into Expert\nSessions so it holds there too. The shared key proves the workspace\nsent it; `actorEmail` is who pressed the button and is recorded in the\naudit row. Emails are normalised (trimmed, lower-cased). An email with\nno Expert Sessions account yet stores the grant as PENDING, applied\nwhen the account is first seen (`userExists: false`), so a grant made\nbefore someone's first sign-in is not lost. Idempotent: repeating a\ncall changes nothing and writes no second audit row. Errors use the\n`{ error: { code, message } }` envelope with lowercase codes; every\nresponse is `Cache-Control: no-store`.\n","security":[{"forlocoInternalAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","sales","actorEmail"],"properties":{"email":{"type":"string","format":"email","maxLength":320},"sales":{"type":"boolean"},"actorEmail":{"type":"string","format":"email","maxLength":320,"description":"The workspace session's verified primary email — set by the calling route, never taken from a browser request body."}}}}}},"responses":{"200":{"description":"The capability as it now stands.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["email","sales","userExists"],"properties":{"email":{"type":"string","format":"email"},"sales":{"type":"boolean"},"userExists":{"type":"boolean","description":"False when the grant was stored as pending for an email with no Expert Sessions account yet."}}}}}}}},"400":{"description":"invalid_request — body is not JSON or fails validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"unauthorized — missing or wrong internal key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"not_found — FEATURE_WORKSPACE_EMBED is off (indistinguishable from unshipped).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"internal_error — unexpected failure, no detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"not_configured — HOURLY_INTERNAL_KEY unset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/internal/consultants/{profileId}/full":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"put":{"operationId":"forlocoSetEngineerFull","tags":["forloco"],"summary":"Mark an engineer full, or available again, on behalf of a named person","description":"Backs the full/available switch on the workspace's shared engineer card\n(US-AD-81, ADR-047). The shared key proves the workspace sent the call;\nit does not make the person who pressed the button allowed to press it.\nExpert Sessions decides that from `actorEmail`: an Expert Sessions\nADMIN, or a person holding Sales (on their account, or as a grant still\npending because they have never opened Expert Sessions). Anyone else\ngets 403 `forbidden`, checked BEFORE the profile is looked up so a\nrefused caller learns nothing about which ids exist.\n\n`full: true` stamps `markedFullAt` to now and records the actor;\n`full: false` clears both. A call asking for the state that already\nholds writes nothing and keeps the original stamp, so \"marked full\nsince\" stays the moment someone actually marked it. A real change is\naudited with the actor. Errors use the `{ error: { code, message } }`\nenvelope with lowercase codes; every response is\n`Cache-Control: no-store`.\n","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"profileId","in":"path","required":true,"description":"The engineer's profile id, as the roster read returns it.","schema":{"type":"string","minLength":1,"maxLength":64}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["full","actorEmail"],"properties":{"full":{"type":"boolean"},"actorEmail":{"type":"string","format":"email","maxLength":320,"description":"The workspace session's verified primary email — the person Expert Sessions authorizes, never a browser-supplied value."}}}}}},"responses":{"200":{"description":"The engineer's state after the call.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["profileId","full","markedFullAt","publiclyBookable"],"properties":{"profileId":{"type":"string"},"full":{"type":"boolean"},"markedFullAt":{"type":["string","null"],"format":"date-time","description":"When the engineer was marked full; null when available."},"publiclyBookable":{"type":"boolean","description":"The public booking page's own answer after the change."}}}}}}}},"400":{"description":"invalid_request — body is not JSON or fails validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"unauthorized — missing or wrong internal key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"forbidden — `actorEmail` is neither an Expert Sessions admin nor a Sales holder. The workspace branches on this code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"not_found — unknown or inactive profile (the same answer for both), or FEATURE_WORKSPACE_EMBED is off.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"internal_error — unexpected failure, no detail returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"not_configured — HOURLY_INTERNAL_KEY unset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/internal/onboarding/hours":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoCreditOnboardingHours","tags":["forloco"],"summary":"Credit hours bought through a workspace onboarding portal to the buyer's organization","description":"Fulfilment for a portal purchase (workspace US-OB-33). The workspace has\nalready taken the money through Finances and provisioned the buyer's\nworkspace organization; this makes those hours BOOKABLE in Expert\nSessions.\n\nIDEMPOTENT ON `orderId` — the Finances order id, already the Stripe\nidempotency key for the charge. It is stored on a unique column of the\ntime package, so a retry (including a concurrent one) answers\n`already_credited` and never doubles the pool.\n\nThe body carries HOURS and never a price: the cost basis comes from\nExpert Sessions' own `NEW_CUSTOMER_HOURLY_RATE_CENTS` setting, because\nthe row feeds contractor payouts. The hours land in an ORGANIZATION pool\nmatched on the workspace organization id, so a second purchase tops up\nthe same pool. `engineerEmail` and `slotStart` are validated but do not\nchange the credit.\n\nLegacy envelope: a bare `{ status }` on success and\n`{ \"error\": \"message\" }` on failure.\n","x-legacy-envelope":true,"security":[{"forlocoInternalAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["organizationId","organizationName","adminEmail","hours","orderId"],"properties":{"organizationId":{"type":"string","minLength":1,"maxLength":64,"description":"The buyer's workspace organization id — the pool's match key."},"organizationName":{"type":"string","minLength":1,"maxLength":200},"adminEmail":{"type":"string","format":"email","maxLength":254},"hours":{"type":"integer","minimum":1,"maximum":2000},"engineerEmail":{"type":["string","null"],"format":"email","maxLength":254,"description":"Roster email of the engineer whose time was bought, if one was chosen."},"slotStart":{"type":["string","null"],"format":"date-time","description":"The slot the buyer picked, if any."},"orderId":{"type":"string","minLength":1,"maxLength":64,"description":"The Finances order id — the idempotency key for the whole credit."}}}}}},"responses":{"200":{"description":"Credited now, or already credited by an earlier call with the same `orderId`. Bare object, NOT the `{ data }` envelope.","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["credited","already_credited"]}}}}}},"400":{"description":"Invalid request — body is not JSON, fails validation, or has unknown keys."},"401":{"description":"Bad or missing shared key."},"404":{"description":"FEATURE_WORKSPACE_EMBED is off — indistinguishable from unshipped."},"500":{"description":"Unexpected failure (no detail returned)."},"503":{"description":"Not configured — the internal key is unset, or the client hourly rate setting is missing or not positive (Expert Sessions refuses to credit rather than guess a cost basis)."}}}},"/api/internal/engineer-availability":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"get":{"operationId":"forlocoEngineerAvailability","tags":["forloco"],"summary":"When a named engineer is free, for the workspace's onboarding portal and Hourly board","description":"Expert Sessions answers from the same engine as the engineer's public\nbooking page (its ADR S-027): weekly hours minus bookings, time off, the\nbusy blocks of the calendar the engineer linked through StackOne, and\nfull weeks. The workspace reads no calendar itself (US-OB-32, US-OB-36).\n\n`email` is the engineer's address off the roster, forwarded server to\nserver; it is never echoed back. `from`/`to` are whole days in the\nVIEWER's zone, default today and thirty days on, capped at forty-five.\n`availability: null` means \"cannot say\" — unknown, inactive or unapproved\nengineer, no linked calendar, or a calendar that did not answer — and an\nempty day list is a real answer. Slot starts only: no busy blocks, no\ntitles.\n","security":[{"forlocoInternalAuth":[]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}},{"name":"timezone","in":"query","required":true,"description":"IANA zone the days and bounds are read in.","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"durationMinutes","in":"query","required":false,"schema":{"type":"integer","minimum":15,"maximum":480,"default":60}}],"responses":{"200":{"description":"The engineer's open slot starts, or null when Expert Sessions cannot say.","content":{"application/json":{"schema":{"type":"object","required":["availability"],"properties":{"availability":{"nullable":true,"type":"object","required":["timezone","durationMinutes","days"],"properties":{"timezone":{"type":"string"},"durationMinutes":{"type":"integer"},"days":{"type":"array","items":{"type":"object","required":["date","starts"],"properties":{"date":{"type":"string","description":"yyyy-MM-dd in the requested timezone"},"starts":{"type":"array","items":{"type":"string","format":"date-time"}}}}}}}}}}}},"400":{"description":"A malformed query (no email, an unknown timezone, a window that ends before it starts)."},"401":{"description":"Bad shared key."},"404":{"description":"The workspace embed is off on Expert Sessions (the route behaves as absent)."},"503":{"description":"Expert Sessions has no shared key configured."}}}},"/api/webhooks/stackone":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoStackOneWebhook","tags":["forloco"],"summary":"StackOne account lifecycle events (an engineer's linked calendar)","description":"`account.created` / `account.updated` record the calendar account an\nengineer just linked in StackOne's hosted Hub; `account.deleted` forgets\nit (Expert Sessions ADR S-027). HMAC-SHA256 over the raw body in\n`x-stackone-signature`, verified against `STACKONE_WEBHOOK_SECRET`; 503\nuntil that secret is set; every other connector's events are\nacknowledged and ignored.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Processed or acknowledged as not ours."},"401":{"description":"Bad or missing signature."},"503":{"description":"No webhook secret configured."}}}},"/api/webhooks/communications":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoCommunicationsWebhook","tags":["forloco"],"summary":"Log an off-platform email against a client (Make scenario)","description":"One JSON object per message from a Make scenario watching a project\nmailbox (Expert Sessions US-SYS-49, ADR S-023). Authenticated by the\n`x-communications-secret` header; 404 while\n`FEATURE_COMMUNICATIONS_LOG` is off. Idempotent on source + messageId.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"`{ id, matched, duplicate }`, including for a message already logged."},"401":{"description":"Bad or missing secret."},"404":{"description":"The communications log is off."}}}},"/api/webhooks/stripe":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoStripeWebhook","tags":["forloco"],"summary":"Stripe events (signature-verified, idempotent)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Stripe event envelope (verified via Stripe-Signature header)."}}}},"responses":{"200":{"description":"Processed."},"400":{"description":"Bad signature."}}}},"/api/webhooks/docusign":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoDocusignWebhook","tags":["forloco"],"summary":"DocuSign envelope events","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Processed."}}}},"/api/webhooks/resend":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoResendWebhook","tags":["forloco"],"summary":"Resend email delivery events","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Processed."}}}},"/api/webhooks/zoom/events":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoZoomWebhook","tags":["forloco"],"summary":"Zoom meeting/recording events (webhook secret verified)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Processed."}}}},"/api/webhooks/airtable/booking-overage":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoAirtableOverageWebhook","tags":["forloco"],"summary":"Airtable booking-overage hook (signed)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Processed."}}}},"/api/webhooks/airtable/tasks":{"x-service":["forloco"],"servers":[{"url":"{forlocoBaseUrl}","description":"Expert Sessions (forloco)","variables":{"forlocoBaseUrl":{"default":"https://for-lo-co-production.up.railway.app"}}}],"post":{"operationId":"forlocoAirtableTasksWebhook","tags":["forloco"],"summary":"Airtable Tasks-table change ping (signed)","description":"Airtable's ping when a watched field on the Tasks table changes,\nreplacing the nightly hours-overage sweep: an ops edit to `Hours Used`\nnow reaches the engineer's approval flow within minutes. The\nnotification body carries NO change data — Airtable only says\n\"something changed\" — so the work is pulled from the stored cursor.\nProtected by HMAC-SHA256 over the raw body against the webhook's own\nbase64-decoded mac secret (matched against the `hmac-sha256=<hex>` form\nAirtable sends), per-IP rate limiting, and the cursor itself, which\nbounds the work per hit. 202 is an acknowledgement — the work is done\ninline, and Airtable retries anything that is not 2xx.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Airtable webhook notification ping (no change payload)."}}}},"responses":{"202":{"description":"Accepted — `{ data: … }` with what the cursor drained."},"401":{"description":"Missing/invalid `x-airtable-content-mac` signature."},"429":{"description":"Per-IP rate limit (60/min) — bounds signature-verification CPU."},"500":{"description":"Processing failed; the cursor did not advance, so the retry re-reads it."},"503":{"description":"Webhook secret or id unset — fails closed and loudly, never a silent 200."}}}},"/api/v1/decisions":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-caller"],"summary":"Create a decision","description":"`Idempotency-Key` is REQUIRED. Replaying the same key with an identical\npayload returns the original 201; a different payload returns\n409 `idempotency_conflict`. An unknown `roleSlug` returns 400\n`unknown_role` with `error.details.validRoles`.\n","security":[{"decideCallerKey":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationOrUnknownRole"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}},"operationId":"decidePostDecisions"},"get":{"tags":["decide-caller"],"summary":"List the caller's decisions","security":[{"decideCallerKey":[]}],"parameters":[{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/DecisionStatus"}},{"name":"roleSlug","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Page of decisions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionListEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"operationId":"decideGetDecisions"}},"/api/v1/decisions/{id}":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-caller"],"summary":"Get a decision (optionally long-polling)","description":"With `?wait=1..60` the request is held until the decision's status\nchanges or the wait elapses; either way the response is 200 with the\ncurrent state and `X-Decide-Long-Poll: changed|timeout`.\nLong-poll is caller-plane only — the Workspace BFF never proxies `wait`.\n","security":[{"decideCallerKey":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"name":"wait","in":"query","schema":{"type":"integer","minimum":1,"maximum":60}}],"responses":{"200":{"description":"Current decision state","headers":{"X-Decide-Long-Poll":{"schema":{"type":"string","enum":["changed","timeout"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"operationId":"decideGetDecisionsId"},"delete":{"tags":["decide-caller"],"summary":"Cancel a decision (pending/needs_info only)","security":[{"decideCallerKey":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decideDeleteDecisionsId"}},"/api/v1/decisions/{id}/info-response":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-caller"],"summary":"Answer a decider's request for more information","description":"Returns the decision to `pending` and re-notifies assignees. 409 unless the decision is `needs_info`.","security":[{"decideCallerKey":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["infoRequestId","answer"],"properties":{"infoRequestId":{"type":"string"},"answer":{"type":"string","maxLength":8192,"description":"Markdown"}}}}}},"responses":{"200":{"description":"Back to pending","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostDecisionsIdInfoResponse"}},"/api/v1/templates/{slug}/decisions":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-caller"],"summary":"Raise a decision from a pre-configured template","description":"The webhook shape external systems point at: nothing but the template\nslug and a flat string param map. The TEMPLATE supplies the role, the\ncopy, the options, and the per-option actions Decide executes on\nresolve — so what an approval *does* is administered in Decide, not\nencoded in the caller.\n\nThe rendered payload funnels through the exact same validation and\ncreation service as `POST /api/v1/decisions`: a templated decision can\nnever do anything a direct one could not. `Idempotency-Key` is\nREQUIRED, and rendering happens BEFORE the idempotency reservation, so\na bad placeholder or a missing record-id param never burns the caller's\nkey. An unknown or archived slug returns 400 `unknown_template` with\n`error.details.validTemplates`.\n","security":[{"decideCallerKey":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]+(-[a-z0-9]+)*$","maxLength":50}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateRaise"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationOrUnknownTemplate"},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}},"operationId":"decidePostTemplateDecisions"}},"/api/v1/webhooks":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-caller"],"summary":"Register a webhook endpoint","description":"The signing `secret` is returned ONCE in this response.","security":[{"decideCallerKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"https only"}}}}}},"responses":{"201":{"description":"Registered (secret included once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"operationId":"decidePostWebhooks"},"get":{"tags":["decide-caller"],"summary":"List webhook endpoints (secrets omitted)","security":[{"decideCallerKey":[]}],"responses":{"200":{"description":"Endpoints","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListEnvelope"}}}}},"operationId":"decideGetWebhooks"}},"/api/v1/webhooks/{id}":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"delete":{"tags":["decide-caller"],"summary":"Disable a webhook endpoint","security":[{"decideCallerKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Disabled"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"operationId":"decideDeleteWebhooksId"}},"/api/v1/workspace/inbox":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"Decisions routed to the identified decider","description":"Decisions whose roleSlug the AUTH-4 `sub` is assigned to, plus direct assignments.","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"status","in":"query","schema":{"$ref":"#/components/schemas/DecisionStatus"}},{"name":"roleSlug","in":"query","schema":{"type":"string"}},{"name":"urgency","in":"query","schema":{"$ref":"#/components/schemas/Urgency"}},{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"Page of decisions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionListEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"operationId":"decideGetWorkspaceInbox"}},"/api/v1/workspace/decisions/{id}":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"Decision detail (info thread + assignment history)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"}],"responses":{"200":{"description":"Detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"404":{"$ref":"#/components/responses/NotFound"}},"operationId":"decideGetWorkspaceDecisionsId"}},"/api/v1/workspace/decisions/{id}/choose":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Choose an option","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["optionId"],"properties":{"optionId":{"type":"string"},"rationale":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Resolved (chosen)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdChoose"}},"/api/v1/workspace/decisions/{id}/request-info":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Ask the caller for more information","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["question"],"properties":{"question":{"type":"string","minLength":1,"maxLength":2000}}}}}},"responses":{"200":{"description":"Now needs_info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdRequestInfo"}},"/api/v1/workspace/decisions/{id}/reassign":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Reassign to another role or member (exactly one of toRoleSlug/toEmail)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"toRoleSlug":{"type":"string"},"toEmail":{"type":"string","format":"email"},"note":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Reassigned (status unchanged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationOrUnknownRole"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdReassign"}},"/api/v1/workspace/decisions/unrouted":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"Open decisions in the org that nobody can act on (admin-guarded at the BFF)","description":"Status pending|needs_info, no direct assignee, and a role that is\narchived, missing, or has zero active members. Each item carries\n`routing`. `count` is the org's total, capped at 1000.\n","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"A page of unrouted decisions","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["decisions","count"],"properties":{"decisions":{"type":"array","items":{"$ref":"#/components/schemas/Decision"}},"nextCursor":{"type":"string"},"count":{"type":"integer","minimum":0,"maximum":1000}}}}}}}}},"operationId":"decideGetWorkspaceDecisionsUnrouted"}},"/api/v1/workspace/decisions/{id}/route":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Route an UNROUTED decision to a staffed role or a member (never decides it)","description":"Admin repair for a decision nobody can act on. Refused with 409\n`decision_routed` when someone already can, 409 `role_unassigned` when\nthe target role has no active members, and 409 `invalid_transition`\nwhen the decision is closed.\n","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"toRoleSlug":{"type":"string"},"toEmail":{"type":"string","format":"email"},"note":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Routed (status unchanged)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationOrUnknownRole"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdRoute"}},"/api/v1/workspace/decisions/{id}/actions/retry":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Re-run a decision's failed template actions","description":"Re-queues dead-lettered actions (attempts reset) and pulls retrying\nones forward. 409 `nothing_to_retry` when nothing failed.\n","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Re-queued; the decision carries `actions`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdActionsRetry"}},"/api/v1/workspace/decisions/{id}/decline":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Decline / abstain","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/DecisionId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"rationale":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Resolved (declined)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecisionEnvelope"}}}},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceDecisionsIdDecline"}},"/api/v1/workspace/stats":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"Pending/urgent counts for the identity","security":[{"decideApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"Counts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"pendingCount":{"type":"integer"},"urgentCount":{"type":"integer"}}}}}}}}},"operationId":"decideGetWorkspaceStats"}},"/api/v1/workspace/roles":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"List decision roles for the org","security":[{"decideApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"Roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleListEnvelope"}}}}},"operationId":"decideGetWorkspaceRoles"},"post":{"tags":["decide-workspace"],"summary":"Create a decision role","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleEnvelope"}}}}},"operationId":"decidePostWorkspaceRoles"}},"/api/v1/workspace/roles/{id}":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"patch":{"tags":["decide-workspace"],"summary":"Update a role (name/description; slug immutable)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"description":{"type":"string","maxLength":400}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleEnvelope"}}}}},"operationId":"decidePatchWorkspaceRolesId"},"delete":{"tags":["decide-workspace"],"summary":"Archive a role (pending decisions keep their history)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Archived"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decideDeleteWorkspaceRolesId"}},"/api/v1/workspace/roles/{id}/assignments":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"List member assignments for a role","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Assignments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignmentListEnvelope"}}}}},"operationId":"decideGetWorkspaceRolesIdAssignments"},"put":{"tags":["decide-workspace"],"summary":"Replace the full member-email list (self-healing on every save)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["emails"],"properties":{"emails":{"type":"array","maxItems":100,"items":{"type":"string","format":"email"}}}}}}},"responses":{"200":{"description":"Replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignmentListEnvelope"}}}}},"operationId":"decidePutWorkspaceRolesIdAssignments"}},"/api/v1/workspace/templates":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"List the org's decision templates (archived excluded)","security":[{"decideApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"Templates","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateListEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"operationId":"decideGetWorkspaceTemplates"},"post":{"tags":["decide-workspace"],"summary":"Create a decision template","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateEnvelope"}}}},"400":{"description":"validation_error — a duplicate slug included","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePostWorkspaceTemplates"}},"/api/v1/workspace/templates/{id}":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"One template with its options and per-option actions","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Template","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"operationId":"decideGetWorkspaceTemplatesId"},"patch":{"tags":["decide-workspace"],"summary":"Update a template (slug immutable; options replace wholesale)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateUpdate"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}},"operationId":"decidePatchWorkspaceTemplatesId"},"delete":{"tags":["decide-workspace"],"summary":"Archive a template (in-flight decisions keep their snapshot)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Archived"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}},"operationId":"decideDeleteWorkspaceTemplatesId"}},"/api/v1/workspace/keys":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"get":{"tags":["decide-workspace"],"summary":"List caller API keys (prefix + metadata only)","security":[{"decideApiKey":[],"workspaceToken":[]}],"responses":{"200":{"description":"Keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyListEnvelope"}}}}},"operationId":"decideGetWorkspaceKeys"},"post":{"tags":["decide-workspace"],"summary":"Create a caller API key (plaintext returned once)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyCreatedEnvelope"}}}}},"operationId":"decidePostWorkspaceKeys"}},"/api/v1/workspace/keys/{id}/rotate":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Rotate a key (new plaintext returned once; old key revoked)","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Rotated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyCreatedEnvelope"}}}}},"operationId":"decidePostWorkspaceKeysIdRotate"}},"/api/v1/workspace/keys/{id}/revoke":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"tags":["decide-workspace"],"summary":"Revoke a key","security":[{"decideApiKey":[],"workspaceToken":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyEnvelope"}}}}},"operationId":"decidePostWorkspaceKeysIdRevoke"}},"/api/v1/internal/expire":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"operationId":"decideInternalExpire","tags":["internal-cron"],"summary":"Expire overdue decisions (scheduled job)","security":[{"decideApiKey":[]}],"responses":{"200":{"description":"`{ data: … }` expired count."}}}},"/api/v1/internal/drain":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"operationId":"decideInternalDrain","tags":["internal-cron"],"summary":"Drain the outbound event outbox (scheduled job)","security":[{"decideApiKey":[]}],"responses":{"200":{"description":"`{ data: … }` drained count."}}}},"/api/v1/mcp":{"x-service":["decide"],"servers":[{"url":"{decideBaseUrl}","description":"Decide","variables":{"decideBaseUrl":{"default":"https://xr-decide-production.up.railway.app"}}}],"post":{"operationId":"decideMcp","tags":["decide-caller"],"summary":"MCP endpoint — request decisions from agent frameworks","security":[{"decideCallerKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"JSON-RPC 2.0 `tools/call` envelope."}}}},"responses":{"200":{"description":"JSON-RPC response."}}}}},"webhooks":{"decisionUpdate":{"post":{"summary":"Delivered to each registered caller endpoint on needs_info/resolved/expired/cancelled","description":"Signed like the workspace inbound event bus:\n`X-Decide-Signature: v1=<hex HMAC-SHA256 over \"<X-Decide-Timestamp>.<rawBody>\">`,\n±300s skew. 5 attempts with exponential backoff, then dead-letter.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["event","decision"],"properties":{"event":{"type":"string","enum":["needs_info","resolved","expired","cancelled"]},"decision":{"$ref":"#/components/schemas/Decision"}}}}}},"responses":{"200":{"description":"Acknowledged (any 2xx)"}}}}}}