{
  "openapi": "3.1.0",
  "info": {
    "title": "ProjexCloud Tenant API Library",
    "version": "2026.08.02",
    "description": "The ProjexCloud API library: every endpoint a tenant application may call, across\n77 SDKs. Generated from the same api_definitions the platform tests against,\nso it cannot describe a route that is not served.\n\nAUTHENTICATION. Most endpoints take a tenant JWT (`Authorization: Bearer <jwt>`)\nobtained from POST /api/auth/login. Machine-to-machine integrations use an application\nAPI key instead (`X-API-Key: pk_live_...`), minted per application so a credential can\nbe revoked without disturbing the others. The gateway is DEFAULT-DENY: an endpoint is\nreachable unauthenticated only if it appears here with `security: []`.\n\nOperator routes (/admin/*) and platform-internal SDKs are deliberately excluded: they reject a tenant JWT, so publishing them would advertise calls that can only fail.",
    "license": {
      "name": "Proprietary — ProjexCloud"
    }
  },
  "servers": [
    {
      "url": "https://cloud.projexlight.com",
      "description": "ProjexCloud managed gateway"
    },
    {
      "url": "http://localhost:4000",
      "description": "Local development gateway"
    }
  ],
  "tags": [
    {
      "name": "connector-twilio-voice",
      "description": "List the tenant's call legs, newest first, optionally filtered by status, direction, or is_voicemail. is_voicemail=true isolates the calls that answering-machine detection classified as reaching voicemail rather than a person. Edge cases: 400 when the tenant_id query param is missing; a tenant with "
    },
    {
      "name": "hdk-camera",
      "description": "Returns the hdk-camera native capability manifest — supported photo formats (jpeg/heic/raw_dng), video codecs (h264/hevc/av1), max photo (48MP) and video (4K@60) resolution, flash/depth-sensor/OCR-passthrough flags, and the iOS/Android native module names the JS bridge must load. The handler is a pu"
    },
    {
      "name": "hdk-diagnostic",
      "description": "Drains the persistent hdk-diagnostic outbox: claims up to `limit` rows from hdk_diagnostic.event where drained_at IS NULL (oldest received_at first, FOR UPDATE SKIP LOCKED), stamps drained_at = now(), and returns the claimed rows plus their count with 200. The body is optional - an absent body or ab"
    },
    {
      "name": "hdk-idp",
      "description": "Registers (upserts) a device identity claim binding a device_uuid to a person_id, optionally storing a biometric template envelope and/or PIN envelope (base64 blobs). Requires device_uuid and person_id; envelopes are optional (a claim with neither credential is accepted). Idempotent upsert on (devic"
    },
    {
      "name": "hdk-image-editor",
      "description": "Returns the hdk-image-editor capability manifest — available tools (crop/rotate/flip/markup/text/blur/sharpen/filter), the five named filters, supported output formats (jpeg/png/heic), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no body, params or qu"
    },
    {
      "name": "hdk-map",
      "description": "Returns the hdk-map capability manifest — supported overlay types (pin/polyline/polygon/heatmap/cluster), gestures, offline-caching and geofencing flags, routing modes (driving/walking/cycling/transit), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no "
    },
    {
      "name": "hdk-measure",
      "description": "Lists every measurement recorded for one capture, filtered by the required capture_id query param and returned as { success: true, data: [...] }. QA edge cases: omitting capture_id is a 400, but an unknown or non-existent capture_id is NOT — it returns 200 with an empty array, so \"no such capture\" a"
    },
    {
      "name": "hdk-permissions",
      "description": "Returns the most recent permission-surface snapshot captured for a device - the newest hdk_permissions.surface_snapshot row for that device_uuid by taken_at DESC - including snapshot_id, tenant_id, persona_id, the permission_set JSONB and taken_at. Edge cases: unlike the sibling claims list, a devic"
    },
    {
      "name": "hdk-scanner",
      "description": "Returns the hdk-scanner capability manifest — supported barcode symbologies (qr_code, pdf_417, code_128, ean_13, data_matrix), document-detection and OCR flags, and the iOS/Android native module names, so JS can negotiate scanner availability without calling into native code. The handler is a pure s"
    },
    {
      "name": "hdk-sync",
      "description": "Resolves a two-sided conflict for an event_type by applying that type's registered policy (crdt / lww / merge / event-sourcing / human-review) to input_a and input_b, optionally linking the outcome to a batch_id and an audit_entry_id. Edge cases: the event_type MUST already be registered via PUT /ap"
    },
    {
      "name": "hdk-video-editor",
      "description": "Returns the hdk-video-editor capability manifest — available tools (trim/merge/caption/mute/speed/overlay), output formats (mp4/hevc/webm), max resolution (4K), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no body, params or query, touches no database"
    },
    {
      "name": "hdk-watermark",
      "description": "Lists every watermark application recorded against one media variant, filtered by the required variant_id query param. QA edge cases: a missing variant_id is a 400, but an unknown variant_id returns 200 with an empty array rather than a 404 — \"no such variant\" and \"variant with no watermarks\" are in"
    },
    {
      "name": "registry-mcp",
      "description": "Single dispatch entry point for the registry MCP service: the caller names a tool and passes its arguments, and the service routes to either a read dispatcher or a write dispatcher depending on whether the name is in the write-tool set. Authentication accepts EITHER an Authorization bearer token or "
    },
    {
      "name": "sdk-agent-runtime",
      "description": "Lists agent_definition rows, optionally filtered by tenant_id and tier, with limit/offset paging. Requires a valid tenant JWT (requireAuth). Edge cases: tenant_id is read from the query string, not the JWT, so an absent tenant_id lists across tenants and a foreign tenant_id is NOT rejected - verify "
    },
    {
      "name": "sdk-ai-gateway",
      "description": "Non-streaming LLM completion. Pipeline: route resolution -> PII redaction -> credential unwrap -> retry-wrapped provider call -> ai_gateway.completion insert + ai-gateway.complete.v1 audit. Closes AC-1."
    },
    {
      "name": "sdk-analytics",
      "description": "Exports an already-materialized dataset build to the warehouse / object store, optionally targeting a named destination via body.target (omitting it uses the configured default). Edge cases: an unknown build_id or a build owned by another tenant returns 404; the body is entirely optional; re-exporti"
    },
    {
      "name": "sdk-api-keys",
      "description": "Issues a new API key for a tenant with an explicit scope list and optional rate limit and expiry, returning 201 with the key record plus the one-time plaintext secret. Edge cases: tenant_id is required and scopes must be a non-empty array of strings — an empty array, a non-array, or an array contain"
    },
    {
      "name": "sdk-approval",
      "description": "Gateway-composed inbox of PENDING approval requests for a tenant, oldest-first. Optional ?assignee_persona_id= narrows it to requests having at least one undecided step assigned to that persona — note assignment lives on approval.step, not on the request. Edge cases: ?tenant_id= is REQUIRED (400 if "
    },
    {
      "name": "sdk-asset",
      "description": "Lists the commands issued against one asset, scoped to the caller's tenant taken from the JWT tenant_id claim, and returns 200 with the array. Because the tenant comes from the token, a caller cannot read another tenant's command history through this route. Edge cases: a token with no tenant_id clai"
    },
    {
      "name": "sdk-assignment",
      "description": "Auto-assign a task to the best-fit persona. The skill, availability-window and per-day capacity gates always run first; then the chosen strategy selects the winner. strategy='default' (or omitted) keeps the primary->backup + distance + capacity scoring; strategy='round_robin' cycles fairly through e"
    },
    {
      "name": "sdk-audit",
      "description": "Appends a hash-chained immutable entry to a per-pool audit chain, updating audit.chain_head in the same transaction; returns 201 with the new entry id/seq/hashes. actor_id comes from the JWT subject (defaults \"unknown\"); actor_kind defaults \"human\". Edge cases: missing pool_index/event_type/payload,"
    },
    {
      "name": "sdk-billing",
      "description": "Generates an invoice for the caller's tenant over a closed billing period using a named rate catalog, returning 201 with the invoice and its line items. tenant_id is always taken from the verified JWT and never from the body, because invoice generation exposes financial PII. Edge cases: catalog_id i"
    },
    {
      "name": "sdk-campaign",
      "description": "Creates a journey (an ordered step list stored as opaque jsonb) under a campaign. The handler does no body validation: an omitted steps array defaults to [], and step contents are never schema-checked here - downstream workers interpret them. Edge cases: an unknown or non-UUID campaign_id fails the "
    },
    {
      "name": "sdk-command",
      "description": "Ingests a command acknowledgement/result FROM the robot edge agent. Unlike the other command routes this handler has no requireAuth preHandler: the bearer token is expected to be the per-robot scoped credential minted by POST /api/assets/:asset_id/credentials and is verified inside ackCommandWithCre"
    },
    {
      "name": "sdk-config",
      "description": "Lists active config rows for a scope. scope query param is required; scope_id defaults to the caller's tenant for tenant scope. Tenant JWT required. Edge cases: scope must be platform|tenant|app|app_user (400)."
    },
    {
      "name": "sdk-connectors",
      "description": "Replay dead-lettered connector-sync items from the DLQ (connectors.sync_deadletter). With a deadletter_id it replays that single item (404 if unknown or already resolved); with tenant_id (and optional connector_kind) it bulk-replays every still-dead-lettered item for that tenant and returns replayed"
    },
    {
      "name": "sdk-consent",
      "description": "Returns the consent.purpose catalogue (purpose_id, app_id, description, legal_basis, default_jurisdictions, created_at) ordered by purpose_id and hard-capped at 200 rows. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: tenant_id is required as a query param"
    },
    {
      "name": "sdk-content",
      "description": "Archives a content item (a soft state change; the row and its versions are retained and still readable via GET). Takes no body. Edge cases: an unknown :item_id returns 404; a non-UUID :item_id fails in Postgres as a 500; there is no already-archived guard, so repeating the call returns 200 with the "
    },
    {
      "name": "sdk-conversation",
      "description": "Evaluates whether the tenant may compose on each requested channel, returning allow|review|deny per channel plus an ORDERED, human-readable reason list — never a bare boolean (AC1). It is an action endpoint, not a create, so the success status is 200. The ordering is the contract: reasons a human ca"
    },
    {
      "name": "sdk-coverage",
      "description": "Who catches the work when the primary does not accept in time. acceptance_window_minutes is how long the primary has before it falls to the backup; zero would mean the backup is notified simultaneously, which is a different arrangement and should be said explicitly rather than arrived at through an "
    },
    {
      "name": "sdk-crm",
      "description": "Log a call on the contact/lead timeline with its structured telephony fields: direction, disposition, duration, phone number, recording url + recording consent, and the provider call id. Emits crm.call.missed.v1 when the disposition means nobody picked up (no_answer/busy/failed) and crm.call.logged."
    },
    {
      "name": "sdk-data-credits",
      "description": "Quotes one capability for this tenant before anything is held: the credit price, the credits currently available (balance minus the part already reserved for in-flight requests) and an explicit affordable flag. The flag is stated rather than left to the caller to compute, because a caller that gets "
    },
    {
      "name": "sdk-data-rights",
      "description": "Records the terminal outcome of a single DSAR execution: status is required and is expected to be 'succeeded' or 'failed', with an optional audit_entry_id sealing the proof and an optional error_detail on failure. Edge cases: a missing status -> 400 (note the value itself is not enum-checked at the "
    },
    {
      "name": "sdk-deliverability",
      "description": "List a tenant's processed provider bounce/complaint events (newest first), optionally filtered by classification. tenant_id query param required."
    },
    {
      "name": "sdk-device",
      "description": "Registers a device, returning 201 with the device record. The insert is an upsert on device_uuid: re-registering an existing device updates os_version, app_version and last_seen_at and COALESCEs device_key_ref (an omitted key ref preserves the stored one) rather than conflicting - so the call is ide"
    },
    {
      "name": "sdk-diagnostic-telemetry",
      "description": "Lists the crash snapshots recorded for one device, selected by the required device_uuid query param. Edge cases: a missing device_uuid query param -> 400; a device_uuid with no crashes is NOT a 404 - it returns 200 with an empty data array; the endpoint accepts no pagination or time-window params, s"
    },
    {
      "name": "sdk-dispatch",
      "description": "P7 FR-DSP-3 — optimizes a dispatcher's stop order for a set of tasks: loads each task's lat/lng, runs nearest-neighbour plus a 2-opt improvement pass, estimates drive time from DISPATCH_AVG_SPEED_KMH (default 35), persists a dispatch.route row and returns {route_id, persona_id, stops[], optimized_at"
    },
    {
      "name": "sdk-engagement",
      "description": "Fetches one encounter by encounter_id, returning the full row: state, vault_key_ref, opened_at/closed_at/sealed_at, retention_policy, retention_expires_at, and the parent/address/billing references. QA edge cases: an unknown encounter_id returns a bare 404 { error: 'NotFound' } with no details array"
    },
    {
      "name": "sdk-event",
      "description": "Checks a ticket in at the door by its qr_token, atomically transitioning the ticket issued -> used and inserting a checkin row; a UNIQUE constraint on checkin.ticket_id backstops double check-in. qr_token and checked_in_by_persona_id are required; device_uuid is optional and stored as null when abse"
    },
    {
      "name": "sdk-evidence",
      "description": "Lists every evidence capture belonging to one encounter, selected by the required encounter_id query param. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: encounter_id is mandatory and its absence (or an empty value) is a 400, since the route would otherwi"
    },
    {
      "name": "sdk-feature-flags",
      "description": "Evaluates a flag for an evaluation context (tenant_id, persona_id, bu_id, arbitrary attributes) and returns resolved_value, matched_rollout_id and kill_switch_engaged. Resolution order: kill switch first, then active rollouts ordered tenant-specific-first by ascending priority whose predicate matche"
    },
    {
      "name": "sdk-geo",
      "description": "Reads one canonical address row by address_id and returns 200 with { data: { address } }. Read-only, requires a valid JWT; the record is not tenant-scoped (geo.address is a platform-wide canonical table), so any authenticated caller can read any address_id. Edge cases: unknown but well-formed UUID r"
    },
    {
      "name": "sdk-handoff",
      "description": "List handoffs for a tenant, most-recently-updated first. Tenant-scoped via the required tenant_id query param; optionally filtered by status and/or deal_id, with limit/offset paging (defaults 50/0)."
    },
    {
      "name": "sdk-identity",
      "description": "Verifies an email + password credential and mints a six-layer JWT. Public - the gateway authGate allowlists /api/auth/login, so no bearer token is needed. An optional tenant_id selects the tenant context from the person memberships, and an optional app_id auto-mints the L2 AppIdentity on first per-a"
    },
    {
      "name": "sdk-identity-resolver",
      "description": "Records a steward verdict (approve merges the pair reversibly; reject marks the link rejected and stamps decided_at, feeding review-latency). Needs step_id from steward-review, and the acting persona MUST equal that step's approver or sdk-approval rejects it. 404 when the link is not the caller tena"
    },
    {
      "name": "sdk-import",
      "description": "List a tenant's mapping templates, grouped by slug with the newest version first, optionally narrowed by slug, kind or active flag. limit is clamped to 1..500 (default 50). Required: tenant_id query param."
    },
    {
      "name": "sdk-incident",
      "description": "List incidents for a tenant, most-recently-updated first. Tenant-scoped via the required tenant_id query param; optionally filtered by status, severity and/or owner_persona_id, with limit/offset paging (defaults 50/0)."
    },
    {
      "name": "sdk-ingest",
      "description": "sdk-ingest batch front door. Accepts an envelope { entity, mode: upsert|insert, idempotency_key, records[] } and imports records into the target entity. Records provenance via sdk-lineage and an append-only entry via sdk-audit. Returns per-record results { imported, skipped, errors[] }."
    },
    {
      "name": "sdk-lead-scoring",
      "description": "Resolves the currently active model for a (tenant_id, vertical) pair, ordered by trained_at DESC NULLS LAST and limited to one row. Both query params are required. QA edge cases: a tenant/vertical pair with no active model returns 404 'no active model' — distinct from the 400 you get when the params"
    },
    {
      "name": "sdk-mcp-bridge",
      "description": "Liveness probe for sdk-mcp-bridge; returns 200 with { sdk: \"sdk-mcp-bridge\", status: \"ok\" }. It is a constant-response handler with no database, no body parsing and no branches, so it cannot fail with a 4xx/5xx. The path ends in /health, so the api-gateway default-deny authGate treats it as public -"
    },
    {
      "name": "sdk-media",
      "description": "Commits an upload: the client reports the SHA-256 it computed and the blob transitions to ready. Returns 200 with the updated blob. checksum_hex must be exactly 64 hex characters (an optional 0x prefix is stripped before the check). The blob's tenant is asserted against the JWT tenant_id claim, so c"
    },
    {
      "name": "sdk-meter",
      "description": "Returns the per-day metered usage rollup for one robot/sensor asset from meter.robot_usage_day, scoped to the tenant on the caller's JWT: rows of asset_id, sensor_id (NULL for asset-level usage), day, sku and units, ordered day DESC then sku ASC, wrapped in a {success, data} envelope. Edge cases: th"
    },
    {
      "name": "sdk-notification",
      "description": "List a tenant's provider delivery-status receipts (newest first), optionally filtered by status. tenant_id query param required."
    },
    {
      "name": "sdk-offer-catalog",
      "description": "Create a stable offer identity (name + unique slug); content lives in immutable versions. Slug is unique per tenant (duplicate -> 409). tenant_id, name and slug required."
    },
    {
      "name": "sdk-parsing",
      "description": "Runs contact extraction over up to 100 captures in one request, each with its own source_kind and raw. An action endpoint returning 200; nothing is persisted. Per-item isolation is the point: ONE malformed item does not fail the batch, because a caller pasting forty signatures should not lose thirty"
    },
    {
      "name": "sdk-payment",
      "description": "Captures a charge against a stored payment method via the provider. Requires a valid tenant JWT (requireAuth); tenant_id is forced from the JWT and any body tenant_id is ignored. The success status is conditional: 201 when the provider result is \"captured\", 200 for any other terminal status (for exa"
    },
    {
      "name": "sdk-persona",
      "description": "Fetches one L2 app_identity row by its app_identity_id path param. Requires a valid tenant JWT (requireAuth). Edge cases: the lookup is by primary key only - the caller JWT tenant is never compared to the row, so any authenticated caller can read any app_identity and tenant scoping is not enforced h"
    },
    {
      "name": "sdk-policy",
      "description": "Creates a versioned policy bundle (name + version + IQL source, with optional obligations) per FR-POL-4 and returns 201. Requires a valid tenant JWT (requireAuth). Edge cases: name, version and iql_source are all mandatory presence checks; obligations, when present, must be an object and each sub-fi"
    },
    {
      "name": "sdk-profile",
      "description": "Reads one L2 profile band (the encrypted fields_envelope) for an app identity and band kind, returning 200 with { data: { band } }. band_kind is validated against the closed set profile | preference | notification_routing before the query runs. Edge cases: an unrecognised band_kind is a 400 (not a 4"
    },
    {
      "name": "sdk-projection",
      "description": "Rebuilds a subject's projection FROM the assertion log rather than patching it, and persists a snapshot plus a content hash. An action endpoint and idempotent, so 200 on the first call and on every repeat — a repeat is expected, not an error. Proves all four criteria. AC1: patching would require kno"
    },
    {
      "name": "sdk-rebac",
      "description": "Evaluates whether a subject_persona_id can reach a target_persona_id via a relationship of the given kind, running a bounded graph traversal and returning a decision plus budget usage. Requires subject_persona_id, target_persona_id and kind (all trimmed). Optional budget object, if present, MUST hav"
    },
    {
      "name": "sdk-scheduling",
      "description": "Push a single appointment to a connection's external calendar, creating the external event or updating/cancelling it if already mapped (this is how reschedule/cancel propagate to the provider). Records the appointment <-> external event mapping. tenant_id and connection_id required."
    },
    {
      "name": "sdk-search",
      "description": "POST form of the search query endpoint: accepts a full search DSL body (query/size/from/sort) instead of the flat query-string form, then runs the same ABAC-filtered execution as GET /api/search. tenant_id comes from the verified JWT and effective_scopes are resolved server-side from JWT claims — an"
    },
    {
      "name": "sdk-secrets",
      "description": "Looks up a SecretRef in the catalog by its ref string, which is URL-encoded in the path and decoded by the handler. Returns the catalog record (ref, scope, kms_key_id, rotation metadata) and never the secret material itself. Requires a valid tenant JWT (requireAuth). Edge cases: the ref path segment"
    },
    {
      "name": "sdk-sequence",
      "description": "Create a reusable message template (subject/body per channel) that sequence steps can reference. tenant_id and name are required; channel defaults to 'email'. UNIQUE per (tenant, name)."
    },
    {
      "name": "sdk-service-request",
      "description": "Creates a service-request queue for a tenant with a display name and an optional numeric routing priority, returning the created queue with 201. Edge cases: tenant_id and name are both mandatory and a missing one produces the generic \"missing fields\" ValidationError; priority is optional and default"
    },
    {
      "name": "sdk-sla",
      "description": "Live clocks approaching or past their deadline, ordered by how close that deadline is — a queue that does not put the next breach first is a list rather than a queue. Each row carries minutes_to_due, is_overdue, how many rungs have already fired, the highest severity fired so far and the NEXT rung w"
    },
    {
      "name": "sdk-social",
      "description": "Authorizes a social network handle for a tenant and binds it to the persona permitted to act on it, returning 201 with the handle record. Edge cases: tenant_id, network, external_handle_id and authorized_persona_id are all mandatory and a missing one returns the generic \"missing fields\" ValidationEr"
    },
    {
      "name": "sdk-source-record",
      "description": "Replace a claim by inserting the successor and STAMPING the prior row (status=SUPERSEDED, superseded_by, superseded_at) in one transaction. The prior value, dates, origin and confidence are never touched — the database trigger rejects any other UPDATE and every DELETE — so both claims stay queryable"
    },
    {
      "name": "sdk-taxonomy",
      "description": "Looks up the active extraction schema for a document_kind, resolving the tenant-specific override first and falling back to the platform-default schema when tenant_id is omitted or has no override. Edge cases: document_kind is a required query param and its absence is a 400; tenant_id is optional an"
    },
    {
      "name": "sdk-tenant",
      "description": "Creates a node in the tenant.geo_node residency tree (region > country > state > city > locality), used to place tenants for data-residency separately from pool_index. Requires name and a kind from the CHECK-constrained enum; residency_class defaults to 'open', code and parent_geo_node_id are option"
    },
    {
      "name": "sdk-tenant-lifecycle",
      "description": "Starts tenant offboarding and stamps a data-retention deadline, defaulting to 30 days from now (per FR-TLC-6) when deadline_at is omitted, and returns the resulting lifecycle state. Edge cases: ownership is checked first — a JWT whose tenant_id and parent_tenant_id both differ from the path tenant_i"
    },
    {
      "name": "sdk-trace",
      "description": "Signed PDF/JSON trace export. Persists trace.export with artifact_s3_key + HMAC signature; emits trace.export.requested.v1 + trace.export.ready.v1 (operational retention)."
    },
    {
      "name": "sdk-vault",
      "description": "Reverses POST /api/vault/encrypt: unwraps the DEK via the secret reference and decrypts the bundle, returning 200 with { plaintext_b64 }. All five of ref, ciphertext_b64, wrapped_dek_b64, iv_b64 and tag_b64 are mandatory non-empty strings - every missing field is reported in the same 400 details arr"
    },
    {
      "name": "sdk-webhook",
      "description": "Lists a tenant's dead-lettered webhook deliveries — the handler delegates to listDlq, so this is the DLQ view rather than all delivery attempts. Edge cases: ?tenant_id= is REQUIRED and is caller-asserted from the query string, not the JWT, so omitting it is a 400; ?limit= is passed through Number() "
    },
    {
      "name": "sdk-workflow",
      "description": "Registers or upserts a workflow definition (name, optional version/namespace, and step_specs) and returns 201 with the stored definition. Requires a valid tenant JWT (requireAuth). Edge cases: name is mandatory and step_specs must be a non-empty array whose every entry is shaped {name: string, compe"
    },
    {
      "name": "semantic-service",
      "description": "Lists every registered cross-domain bridge (semantic.cross_domain_bridge) as {success:true, data:[...]} with bridge_id, from_object_type_id, to_object_type_id, access_mode and requires_cross_tenant_consent. QA edge cases: the handler takes no query parameters — it is a global, unfiltered, unpaginate"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Tenant JWT from POST /api/auth/login. Carries the six-layer claim set (tenant_id, app_id, persona ids); authority comes from the persona and its ReBAC grants rather than from scopes on the token."
      },
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Application credential presented as `Authorization: Bearer pk_live_...` (or pk_test_). Minted per application, so one can be revoked without disturbing the others. live/test is a property of the APPLICATION, not the key, so a test credential can never reach production data. NOTE: credential-management routes (/api/applications, /api/api-keys) deliberately require a human JWT and reject a key — a key that can mint another key cannot be contained by revoking it."
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/agent-runtime/agents": {
      "get": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "List agent definitions",
        "description": "Lists agent_definition rows, optionally filtered by tenant_id and tier, with limit/offset paging. Requires a valid tenant JWT (requireAuth). Edge cases: tenant_id is read from the query string, not the JWT, so an absent tenant_id lists across tenants and a foreign tenant_id is NOT rejected - verify tenant scoping deliberately; limit/offset go through parseInt so a non-numeric or empty value yields NaN and surfaces as a 500 \"List failed\" rather than a 400; an unknown tier value or a tenant with no definitions returns 200 with an empty array, not a 404; a malformed non-UUID tenant_id fails the Postgres uuid cast and becomes a 500.",
        "operationId": "get_api_agent_runtime_agents",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "ListFailed — List failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Create agent definition",
        "description": "Create an agent_definition. Required: name, acting_persona_id, tier, vector_namespace, created_by. Returns the row including server-generated agent_id + timestamps.",
        "operationId": "post_api_agent_runtime_agents",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": null,
                    "name": "Acme QA Sample",
                    "description": "QA automation agent definition",
                    "acting_persona_id": "{{var:acting_persona_id}}",
                    "agent_scope": [],
                    "default_ttl_seconds": 30,
                    "tier": "sync",
                    "kill_switch_flag_id": null,
                    "vector_namespace": "agents-qa-ns-default",
                    "tool_manifest": [
                      "crm.contact.create"
                    ],
                    "created_by": "qa-automation",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Required: name, acting_persona_id, tier, vector_namespace, created_by"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "CreateFailed — Create failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "nullable": true
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "acting_persona_id": {
                    "type": "string"
                  },
                  "agent_scope": {
                    "type": "array",
                    "items": {}
                  },
                  "default_ttl_seconds": {
                    "type": "integer"
                  },
                  "tier": {
                    "type": "string"
                  },
                  "kill_switch_flag_id": {
                    "nullable": true
                  },
                  "vector_namespace": {
                    "type": "string"
                  },
                  "tool_manifest": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "created_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": null,
                "name": "<generated name>",
                "description": "QA automation agent definition",
                "acting_persona_id": "{{var:acting_persona_id}}",
                "agent_scope": [],
                "default_ttl_seconds": 30,
                "tier": "sync",
                "kill_switch_flag_id": null,
                "vector_namespace": "agents-qa-ns-default",
                "tool_manifest": [
                  "crm.contact.create"
                ],
                "created_by": "qa-automation"
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/health": {
      "get": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Agent runtime health check",
        "description": "Static liveness probe for sdk-agent-runtime; returns {sdk:\"sdk-agent-runtime\", status:\"ok\"} with no DB or downstream call. Public: the gateway authGate allowlists any path ending in /health, so it answers 200 with no Authorization header, with a malformed or expired bearer token, and regardless of tenant. Edge cases: the body is a constant so no data path can 404 or 500; query strings and extra headers are ignored; a non-GET verb on this path is a Fastify 404 route-miss rather than a handler error.",
        "operationId": "get_api_agent_runtime_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/agent-runtime/runs": {
      "get": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "List agent runs",
        "description": "Lists agent_run rows, optionally filtered by tenant_id, agent_id and status, with limit/offset paging. Requires a valid tenant JWT (requireAuth). Edge cases: filters come from the query string, not the JWT, so omitting tenant_id lists across tenants and a foreign tenant_id is not rejected - tenant scoping must be tested explicitly; limit/offset go through parseInt, so a non-numeric value becomes NaN and surfaces as a 500 \"List failed\" instead of a 400; an unknown status value or an agent_id with no runs returns 200 with an empty array rather than a 404; a non-UUID tenant_id or agent_id fails the uuid cast and yields a 500.",
        "operationId": "get_api_agent_runtime_runs",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "ListFailed — List failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Start agent run",
        "description": "Start an agent_run. Computes ttl_deadline from agent's default_ttl_seconds, materialises agent_chain from parent_run_id, emits agent.run.started.v1 (regulated retention) with chain as actor provenance.",
        "operationId": "post_api_agent_runtime_runs",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "trace_id": "{{var:trace_id}}",
                    "parent_run_id": null,
                    "ttl_seconds": 300,
                    "actor_id": "{{var:operator_id}}",
                    "actor_kind": "human",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Required: agent_id, persona_id, trace_id, actor_id, actor_kind"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <entity> not found"
          },
          "500": {
            "description": "InternalError — Start run failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "trace_id": {
                    "type": "string"
                  },
                  "parent_run_id": {
                    "nullable": true
                  },
                  "ttl_seconds": {
                    "type": "integer"
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "actor_kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "agent_id": "<id from agent-runtime-agents>",
                "persona_id": "<id from personas>",
                "trace_id": "{{var:trace_id}}",
                "parent_run_id": null,
                "ttl_seconds": 300,
                "actor_id": "{{var:operator_id}}",
                "actor_kind": "human"
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/agents/{id}": {
      "get": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Get agent definition",
        "description": "Fetch an agent_definition by id.",
        "operationId": "get_api_agent_runtime_agents_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing path param: id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — agent_definition not found"
          },
          "500": {
            "description": "LookupFailed — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/agent-runtime/runs/{id}": {
      "get": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Get agent run",
        "description": "Fetch a single agent_run including agent_chain + execution_log_ref.",
        "operationId": "get_api_agent_runtime_runs_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing path param: id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — agent_run not found"
          },
          "500": {
            "description": "InternalError — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/agent-runtime/runs/{run_id}/replay": {
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Replay a freshly-started run (no execution log yet) returns 404",
        "description": "Replay an agent run against the recorded model snapshot. Returns {kind:'matched'|'snapshot-drift'|'diverged', ...}. AC-5. A freshly-started run has no execution_log_entry rows yet (those are appended only by the internal runtime engine, no HTTP producer), so replaying it returns 404 'no execution log entries'.",
        "operationId": "post_api_agent_runtime_runs_run_id_replay",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "400": {
            "description": "ValidationError — Missing path param: run_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "current_model_snapshot_id": null,
                    "dryRun": true,
                    "replay_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "500": {
            "description": "InternalError — Replay failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "current_model_snapshot_id": {
                    "nullable": true
                  },
                  "dryRun": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "current_model_snapshot_id": null,
                "dryRun": true
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/runs/{run_id}/rollback": {
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Roll back a freshly-started run (empty action journal) returns 200",
        "description": "Roll back an agent run by replaying agents.action_journal in reverse from latest step down to ?to_seq=N. Invokes per-action_type compensation handlers. Returns RollbackSummary. AC-8. A freshly-started run has an empty action_journal, so rollback returns 200 with attempted=0.",
        "operationId": "post_api_agent_runtime_runs_run_id_rollback",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "Operator-initiated rollback for testing",
                    "actor_id": "{{var:operator_id}}",
                    "rollback_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing path param: run_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Rollback failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "Operator-initiated rollback for testing",
                "actor_id": "{{var:operator_id}}"
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/tokens/{token_id}/revoke": {
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Revoke a capability token",
        "description": "Revoke a capability token. Idempotent. Mid-flight tools polling isRevoked() observe the new state and self-cancel. FR-ART-4 / AC-3.",
        "operationId": "post_api_agent_runtime_tokens_token_id_revoke",
        "parameters": [
          {
            "name": "token_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "token_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "Operator-initiated revoke for testing",
                    "actor_id": "{{var:operator_id}}",
                    "actor_kind": "human",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "MissingPathParam — Missing path param: token_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "RevokeFailed — Revoke failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "actor_kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "Operator-initiated revoke for testing",
                "actor_id": "{{var:operator_id}}",
                "actor_kind": "human"
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/tokens/{token_id}/validate": {
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Validate freshly-minted token",
        "description": "Validate a capability token against the args the caller intends to invoke. Checks expiry, single-use, revocation, args binding, signature. Returns valid:true|false with reason. Always HTTP 200 (validity is in the body).",
        "operationId": "post_api_agent_runtime_tokens_token_id_validate",
        "parameters": [
          {
            "name": "token_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "token_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "args": {
                      "first_name": "Ada"
                    },
                    "validate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "MissingPathParam — Missing path param: token_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "ValidateFailed — Validate failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "args": {
                    "type": "object",
                    "properties": {
                      "first_name": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "args": {
                  "first_name": "Ada"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent-runtime/tokens": {
      "post": {
        "tags": [
          "sdk-agent-runtime"
        ],
        "summary": "Mint capability token",
        "description": "Mint a capability token for one tool invocation. HMAC-SHA256 signed, scope-limited to (agent_id, persona, tool_sku, args_hash, tenant_scope, expires_at), single-use. FR-ART-1..3. tool_sku MUST be in the agent's tool_manifest or the mint is denied with 403 scope_violation.",
        "operationId": "post_api_agent_runtime_tokens",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "token_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "acting_persona_id": "{{var:acting_persona_id}}",
                    "tool_sku": "crm.contact.create",
                    "args": {
                      "first_name": "Ada"
                    },
                    "tenant_scope": "tenant-default",
                    "ttl_seconds": 60,
                    "actor_id": "{{var:operator_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "MissingRequiredField — Missing required field: run_id, agent_id, acting_persona_id, tool_sku, args, tenant_scope are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "scope_violation — scope_violation"
          },
          "500": {
            "description": "MintFailed — Mint failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "run_id": {
                    "type": "string"
                  },
                  "agent_id": {
                    "type": "string"
                  },
                  "acting_persona_id": {
                    "type": "string"
                  },
                  "tool_sku": {
                    "type": "string"
                  },
                  "args": {
                    "type": "object",
                    "properties": {
                      "first_name": {
                        "type": "string"
                      }
                    }
                  },
                  "tenant_scope": {
                    "type": "string"
                  },
                  "ttl_seconds": {
                    "type": "integer"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "run_id": "<id from agent-runtime-runs>",
                "agent_id": "<id from agent-runtime-agents>",
                "acting_persona_id": "{{var:acting_persona_id}}",
                "tool_sku": "crm.contact.create",
                "args": {
                  "first_name": "Ada"
                },
                "tenant_scope": "tenant-default",
                "ttl_seconds": 60,
                "actor_id": "{{var:operator_id}}"
              }
            }
          }
        }
      }
    },
    "/api/ai-gateway/complete": {
      "post": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Completion records consent + budget + redaction + trace",
        "description": "Non-streaming LLM completion. Pipeline: route resolution -> PII redaction -> credential unwrap -> retry-wrapped provider call -> ai_gateway.completion insert + ai-gateway.complete.v1 audit. Closes AC-1.",
        "operationId": "post_api_ai_gateway_complete",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "complete_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "request": {
                      "model": "gpt-4o-mini",
                      "prompt": "Summarize the onboarding checklist in one sentence.",
                      "max_tokens": 100,
                      "temperature": 0.7,
                      "top_p": 1,
                      "stop_sequences": [],
                      "task_tag": "test",
                      "provider_hint": "openai",
                      "tools": [],
                      "stream": false
                    },
                    "context": {
                      "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "acting_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "span_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "ttl_deadline": "2026-01-15T10:30:00Z",
                      "agent_chain": []
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing required: request, context"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "CompletionFailed — Completion failed"
          },
          "503": {
            "description": "ProviderUnavailable — provider not available / no route matches and no provider_hint supplied"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "request": {
                    "type": "object",
                    "properties": {
                      "model": {
                        "type": "string"
                      },
                      "prompt": {
                        "type": "string"
                      },
                      "max_tokens": {
                        "type": "integer"
                      },
                      "temperature": {
                        "type": "number"
                      },
                      "top_p": {
                        "type": "integer"
                      },
                      "stop_sequences": {
                        "type": "array",
                        "items": {}
                      },
                      "task_tag": {
                        "type": "string"
                      },
                      "provider_hint": {
                        "type": "string"
                      },
                      "tools": {
                        "type": "array",
                        "items": {}
                      },
                      "stream": {
                        "type": "boolean"
                      }
                    }
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "agent_id": {
                        "type": "string"
                      },
                      "run_id": {
                        "type": "string"
                      },
                      "acting_persona_id": {
                        "type": "string"
                      },
                      "tenant_id": {
                        "type": "string"
                      },
                      "trace_id": {
                        "type": "string"
                      },
                      "span_id": {
                        "type": "string"
                      },
                      "ttl_deadline": {
                        "type": "string"
                      },
                      "agent_chain": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                }
              },
              "example": {
                "request": {
                  "model": "gpt-4o-mini",
                  "prompt": "Summarize the onboarding checklist in one sentence.",
                  "max_tokens": 100,
                  "temperature": 0.7,
                  "top_p": 1,
                  "stop_sequences": [],
                  "task_tag": "test",
                  "provider_hint": "openai",
                  "tools": [],
                  "stream": false
                },
                "context": {
                  "agent_id": "<id from agent-runtime-agents>",
                  "run_id": "<id from agent-runtime-runs>",
                  "acting_persona_id": "<id from personas>",
                  "tenant_id": "<id from auth>",
                  "trace_id": "<generated uuid>",
                  "span_id": "<generated uuid>",
                  "ttl_deadline": "<generated futuredatetime>",
                  "agent_chain": []
                }
              }
            }
          }
        }
      }
    },
    "/api/ai-gateway/health": {
      "get": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Health probe returns ok",
        "description": "Liveness probe for sdk-ai-gateway. Returns 200 with the constant body { sdk: 'sdk-ai-gateway', status: 'ok' }. QA edge cases: this path is on the api-gateway public allowlist (isHealth matches any path ending in /health), so it is reachable with NO Authorization header and must NOT return 401 — a 401 here means the authGate allowlist regressed, which is the one regression worth asserting; the handler has no requireAuth preHandler, touches no database, provider or credential store, and takes no parameters, so it reports only that the route is mounted and cannot detect a degraded provider or an unreachable Postgres — 'ok' is not a dependency health signal; it therefore has no error responses of its own.",
        "operationId": "get_api_ai_gateway_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "internal://sdk-ai-gateway/completionService.emitCompletionEvent": {
      "internal_function": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Tenant credential completion stamps credential_source=tenant and bills 0 markup",
        "description": "Internal (non-HTTP) contract for completionService.emitCompletionEvent, which stamps credential_source ('tenant' or 'platform') into the ai-gateway.complete.v1 / ai-gateway.stream.v1 audit payload so the meter ingest worker knows which SKUs to emit (FR-BYOK-9 / AC-4). On a BYOK call the tenant pays the provider directly, so billed_cost is 0, the ai-gateway.tokens.* markup SKU is suppressed, and only the governance SKU is emitted; on a platform-credential call both SKUs are emitted and billed_cost carries the margin. QA edge cases: the entire emit is wrapped in try/catch and only console.errors on failure, so an audit-ledger outage NEVER fails or degrades the completion — the completion still returns 200 while the billing event is silently lost, which is the revenue-leak scenario to watch for; consequently this function has no error return path of its own; the effect is observable only in the audit ledger payload (credential_source) and the persisted ai_gateway.completion.billed_cost, not in any HTTP response body.",
        "operationId": "internal_function_internal_sdk_ai_gateway_completionService_emitCompletionEvent",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "completionService.emitCompletionEvent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scenario": {
                    "type": "string"
                  },
                  "expected_audit_payload_field": {
                    "type": "string"
                  },
                  "expected_billed_cost": {
                    "type": "integer"
                  },
                  "expected_token_sku_emitted": {
                    "type": "boolean"
                  },
                  "expected_governance_sku_emitted": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "scenario": "tenant_credential",
                "expected_audit_payload_field": "credential_source=tenant",
                "expected_billed_cost": 0,
                "expected_token_sku_emitted": false,
                "expected_governance_sku_emitted": true
              }
            }
          }
        }
      }
    },
    "internal://sdk-ai-gateway/completionService.loadProviderRow": {
      "internal_function": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Tenant active binding → tenant credential",
        "description": "Internal (non-HTTP) contract for completionService.loadProviderRow, the BYOK credential resolver behind /api/ai-gateway/complete and /stream. Resolution order per FR-BYOK-2: (1) an active row in ai_gateway.tenant_provider_credential for (tenant, provider) — but if its model_allowlist is non-null and the requested model is absent from it, treat as no tenant credential and fall through (FR-BYOK-6); (2) the platform row in ai_gateway.provider. QA edge cases: the function NEVER throws — it returns null when neither row exists, and the caller (complete/stream) is what raises '[ai-gateway] provider <id> not available', which the controller maps to HTTP 503; the caching behaviour is the subtle part — results are cached per (tenant, provider) with no model in the key, so a model-allowlist fall-through deliberately bypasses and does not poison the cache, and a test that binds an allowlisted credential then immediately requests a non-allowlisted model must still see the platform credential; a disabled platform row is still returned (status is carried on the row, not filtered in the query), so 'disabled provider' is enforced downstream rather than here; a null/undefined tenant_id skips step 1 entirely and resolves platform-only.",
        "operationId": "internal_function_internal_sdk_ai_gateway_completionService_loadProviderRow",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "completionService.loadProviderRow_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "503": {
            "description": "ProviderNotAvailable — [ai-gateway] provider <provider_id> not available"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "provider_id": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "tenant_binding": {
                    "type": "string"
                  },
                  "expected_credential_source": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "{{static:00000000-0000-0000-0000-000000000001}}",
                "provider_id": "openai",
                "model": "gpt-4o",
                "tenant_binding": "active",
                "expected_credential_source": "tenant"
              }
            }
          }
        }
      }
    },
    "/api/ai-gateway/stream": {
      "post": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "SSE stream produces chunks and final completion row",
        "description": "Streaming LLM completion (SSE). Yields StreamChunk frames as data: <json>\\n\\n; closes with data: [DONE]\\n\\n. Persists ai_gateway.completion on stream close.",
        "operationId": "post_api_ai_gateway_stream",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "stream_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "request": {
                      "model": "claude-opus-4-7",
                      "prompt": "Stream a short greeting.",
                      "max_tokens": 100,
                      "temperature": 0.7,
                      "top_p": 1,
                      "stop_sequences": [],
                      "task_tag": "test",
                      "provider_hint": "anthropic",
                      "tools": [],
                      "stream": true
                    },
                    "context": {
                      "agent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "acting_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "span_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "ttl_deadline": "2026-01-15T10:30:00Z",
                      "agent_chain": []
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing required: request, context"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "request": {
                    "type": "object",
                    "properties": {
                      "model": {
                        "type": "string"
                      },
                      "prompt": {
                        "type": "string"
                      },
                      "max_tokens": {
                        "type": "integer"
                      },
                      "temperature": {
                        "type": "number"
                      },
                      "top_p": {
                        "type": "integer"
                      },
                      "stop_sequences": {
                        "type": "array",
                        "items": {}
                      },
                      "task_tag": {
                        "type": "string"
                      },
                      "provider_hint": {
                        "type": "string"
                      },
                      "tools": {
                        "type": "array",
                        "items": {}
                      },
                      "stream": {
                        "type": "boolean"
                      }
                    }
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "agent_id": {
                        "type": "string"
                      },
                      "run_id": {
                        "type": "string"
                      },
                      "acting_persona_id": {
                        "type": "string"
                      },
                      "tenant_id": {
                        "type": "string"
                      },
                      "trace_id": {
                        "type": "string"
                      },
                      "span_id": {
                        "type": "string"
                      },
                      "ttl_deadline": {
                        "type": "string"
                      },
                      "agent_chain": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                }
              },
              "example": {
                "request": {
                  "model": "claude-opus-4-7",
                  "prompt": "Stream a short greeting.",
                  "max_tokens": 100,
                  "temperature": 0.7,
                  "top_p": 1,
                  "stop_sequences": [],
                  "task_tag": "test",
                  "provider_hint": "anthropic",
                  "tools": [],
                  "stream": true
                },
                "context": {
                  "agent_id": "<id from agent-runtime-agents>",
                  "run_id": "<id from agent-runtime-runs>",
                  "acting_persona_id": "<id from personas>",
                  "tenant_id": "<id from auth>",
                  "trace_id": "<generated uuid>",
                  "span_id": "<generated uuid>",
                  "ttl_deadline": "<generated futuredatetime>",
                  "agent_chain": []
                }
              }
            }
          }
        }
      }
    },
    "events://ai_gateway.tenant_credential": {
      "event_contract": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Three event types registered with regulated retention",
        "description": "Contract-only anchor for the three ai_gateway.tenant_credential.{bound,rotated,revoked}.v1 event types declared in packages/contracts/src/events.ts, all registered with regulated retention and an event-sourcing conflict policy. They are emitted as a side effect of the bind (POST), rotate (PATCH) and revoke (DELETE) /api/ai-gateway/tenant-credentials endpoints. QA edge cases: this definition declares no runtime surface — there is no route, no handler and therefore no error response of its own, so producer-side correctness must be asserted through those three endpoints' integration tests rather than here; the security-critical property to verify on the producer side is that no event payload ever carries raw_key or credential_envelope (last_4 and binding metadata only); because retention is 'regulated' these events are subject to the longer retention class, and because emission is best-effort inside the credential service a successful 2xx from bind/rotate/revoke does not by itself prove the event was appended.",
        "operationId": "event_contract_events_ai_gateway_tenant_credential",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ai_gateway.tenant_credential_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_types": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "retention_class": {
                    "type": "string"
                  },
                  "conflict_policy": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "event_types": [
                  "ai_gateway.tenant_credential.bound.v1",
                  "ai_gateway.tenant_credential.rotated.v1",
                  "ai_gateway.tenant_credential.revoked.v1"
                ],
                "retention_class": "regulated",
                "conflict_policy": "event-sourcing"
              }
            }
          }
        }
      }
    },
    "/api/ai-gateway/tenant-credentials/{binding_id}": {
      "delete": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Revoke an active binding with a reason",
        "description": "Revokes an active tenant credential binding (FR-BYOK-6), requiring a human-readable reason of at least 6 characters after trimming — mirroring the CMEK BYOK revoke pattern so the audit ledger always carries a justification. QA edge cases: the reason guard trims first, so a body of six spaces is rejected as a 400 while 'typo!!' is accepted — assert the trim, not just the length; as with rotate, the 404-vs-500 split is decided by testing the thrown error message for the substring 'not found', so an unknown binding_id is a 404 and everything else is a 500; revoking an ALREADY-revoked binding is not idempotent-safe — the second call no longer matches an active row and returns 404 rather than 200, which is the repeat-call case to cover; revocation is a soft state change, so the binding row remains listable with a revoked status rather than disappearing; the important downstream effect is that completions for that (tenant, provider) fall back to the platform credential and start billing the markup SKU again, which is worth asserting end-to-end.",
        "operationId": "delete_api_ai_gateway_tenant_credentials_binding_id",
        "parameters": [
          {
            "name": "binding_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "binding_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — binding_id path param is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error message containing \"not found\">"
          },
          "500": {
            "description": "RevokeFailed — <underlying error message> | revoke failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Rotate the raw key on an existing binding",
        "description": "Rotates the raw API key on an existing active tenant credential binding (FR-BYOK-5). binding_id and bound_at are preserved across the rotation so downstream references stay valid; only the enveloped key material and the rotation metadata change, and the response again never exposes raw_key or credential_envelope. QA edge cases: the 404-vs-500 split is message-driven, not type-driven — the handler inspects the thrown error string for the substring 'not found', so an unknown or already-revoked binding_id returns 404 while every other failure returns 500; that also means a future error message that happens to contain 'not found' would be misclassified; raw_key must be at least 8 characters (the same non-trivial-string guard as bind), so a rotation to an empty or short key is a 400; the endpoint does NOT verify the new key against the provider, so rotating to a syntactically valid but revoked upstream key succeeds here and only fails later at completion time; rotation is not idempotent in effect — each call re-envelopes and re-stamps, and the previous key material is not recoverable afterwards.",
        "operationId": "patch_api_ai_gateway_tenant_credentials_binding_id",
        "parameters": [
          {
            "name": "binding_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "binding_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tenant_credential_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "raw_key": "sk-test-NEWDUMMYKEYAFTERROTATE",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — binding_id path param is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error message containing \"not found\">"
          },
          "500": {
            "description": "RotateFailed — <underlying error message> | rotate failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "raw_key": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "raw_key": "{{static:sk-test-NEWDUMMYKEYAFTERROTATE}}"
              }
            }
          }
        }
      }
    },
    "/api/ai-gateway/tenant-credentials": {
      "get": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "List bindings for a tenant",
        "description": "Lists a tenant's AI provider credential bindings for the tenant-admin UI. Each entry carries the binding lifecycle metadata plus last_4 of the key; credential_envelope is never returned, which is the primary security assertion — a response containing envelope bytes or a full key is a defect. QA edge cases: tenant_id is a REQUIRED query param and is read straight from the URL rather than from the caller's JWT claims, so an authenticated user of tenant A can list tenant B's bindings by changing the query string — that cross-tenant isolation gap is the most valuable test here; an unknown tenant_id returns 200 with an empty bindings array rather than a 404, so 'no such tenant' and 'tenant with no bindings' are indistinguishable; the listing is unpaginated with no limit/offset and no status filter, so revoked bindings are returned alongside active ones and must be filtered client-side; any database failure is flattened to a generic 500 'list failed' with the real cause only in the server log.",
        "operationId": "get_api_ai_gateway_tenant_credentials",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "tenant_credential_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "ListFailed — list failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-ai-gateway"
        ],
        "summary": "Bind an OpenAI key for the tenant",
        "description": "Binds a tenant's own AI provider API key (BYOK, FR-BYOK-3). The raw key is enveloped by the credential service and the response deliberately contains only the binding metadata plus a last_4 — never raw_key and never credential_envelope, which is the primary security assertion for this endpoint. Optional model_allowlist restricts which models may use the tenant key, and fallback_on_error controls whether a failing tenant key falls back to the platform credential. QA edge cases: provider_id must be one of exactly anthropic, openai, bedrock or gemini — anything else is a 400 naming the unsupported value; raw_key must be a string of at least 8 characters, so a short or trimmed-to-empty key is a 400 rather than a provider-side failure at call time; every non-validation failure, including a duplicate active binding for the same (tenant_id, provider_id), surfaces as a 500 rather than a 409, so a re-bind test must expect 500 with the constraint message; the actor recorded on the binding falls back to the literal 'tenant-admin-ui' when the JWT carries no persona_id or sub; a model_allowlist that omits the model actually requested later does not fail here — it silently causes fall-through to the platform credential at completion time.",
        "operationId": "post_api_ai_gateway_tenant_credentials",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tenant_credential_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "provider_id": "openai",
                    "raw_key": "sk-test-DUMMYKEYFORTESTING",
                    "model_allowlist": [
                      "gpt-4o",
                      "gpt-4o-mini"
                    ],
                    "fallback_on_error": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, provider_id, raw_key are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "BindFailed — <underlying error message> | bind failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "provider_id": {
                    "type": "string"
                  },
                  "raw_key": {
                    "type": "string"
                  },
                  "model_allowlist": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "fallback_on_error": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "provider_id": "openai",
                "raw_key": "{{static:sk-test-DUMMYKEYFORTESTING}}",
                "model_allowlist": [
                  "gpt-4o",
                  "gpt-4o-mini"
                ],
                "fallback_on_error": true
              }
            }
          }
        }
      }
    },
    "/api/analytics/builds/{build_id}/export": {
      "post": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "Export a built dataset to the warehouse",
        "description": "Exports an already-materialized dataset build to the warehouse / object store, optionally targeting a named destination via body.target (omitting it uses the configured default). Edge cases: an unknown build_id or a build owned by another tenant returns 404; the body is entirely optional; re-exporting the same build is allowed and simply overwrites/re-emits the artifact; large builds and object-store failures surface as 500.",
        "operationId": "post_api_analytics_builds_build_id_export",
        "parameters": [
          {
            "name": "build_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "build_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "export_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target": "iceberg://warehouse/datasets",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — dataset build not found"
          },
          "500": {
            "description": "InternalError — <error message from exportDatasetBuild>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "target": "iceberg://warehouse/datasets"
              }
            }
          }
        }
      }
    },
    "/api/analytics/datasets": {
      "get": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "List dataset specs for the tenant",
        "description": "Lists every dataset spec belonging to the caller's tenant. Tenant scoping comes from the JWT claim, so a caller can never see another tenant's specs and a token with no tenant claim is rejected 400. Edge cases: a tenant with no specs returns an empty array with 200 (not 404); the endpoint takes no filter/pagination params, so the full spec list is returned each call.",
        "operationId": "get_api_analytics_datasets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "dataset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <error message from listDatasetSpecs>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "Register a per-minute feature dataset spec",
        "description": "Registers an ML feature/training-dataset spec (name + asset_id required; optional sensor_ids, grain, aggregations, label_source). tenant_id is taken from the JWT claim and never from the body, so specs are strictly tenant-scoped and a token without a tenant claim is rejected 400. Edge cases: missing name or asset_id -> 400; an empty or omitted sensor_ids means the spec covers every sensor on the asset; the route does not pre-validate that asset_id/sensor_ids exist, so a bad reference surfaces as a 500 from the insert; the call is NOT idempotent - repeating it with the same name creates another spec.",
        "operationId": "post_api_analytics_datasets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "dataset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "asset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "sensor_ids": [],
                    "grain": "minute",
                    "aggregations": [
                      "avg",
                      "min",
                      "max",
                      "last",
                      "count"
                    ],
                    "label_source": {
                      "kind": "intervals",
                      "default_label": 0,
                      "intervals": []
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <error message from createDatasetSpec>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "asset_id": {
                    "type": "string"
                  },
                  "sensor_ids": {
                    "type": "array",
                    "items": {}
                  },
                  "grain": {
                    "type": "string"
                  },
                  "aggregations": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "label_source": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      },
                      "default_label": {
                        "type": "integer"
                      },
                      "intervals": {
                        "type": "array",
                        "items": {}
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "<generated name>",
                "asset_id": "<id from assets>",
                "sensor_ids": [],
                "grain": "minute",
                "aggregations": [
                  "avg",
                  "min",
                  "max",
                  "last",
                  "count"
                ],
                "label_source": {
                  "kind": "intervals",
                  "default_label": 0,
                  "intervals": []
                }
              }
            }
          }
        }
      }
    },
    "/api/analytics/datasets/{spec_id}/build": {
      "post": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "Build feature windows for a 1h range",
        "description": "Materializes a feature window for a dataset spec over the [from, to] time range, producing a build row with lineage. Both from and to are required. Edge cases: a spec_id that does not exist or belongs to another tenant returns 404 (tenant scoping is applied inside the lookup); an inverted or zero-width from/to window is not rejected at the route and yields an empty/zero-row build; oversized windows and downstream warehouse failures surface as 500; builds are not idempotent - repeating the same window creates another build row in the reproducibility ledger.",
        "operationId": "post_api_analytics_datasets_spec_id_build",
        "parameters": [
          {
            "name": "spec_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "spec_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "build_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "from": "2026-01-15T10:30:00Z",
                    "to": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — dataset spec not found"
          },
          "500": {
            "description": "InternalError — <error message from buildDatasetFromSpec>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from": {
                    "type": "string"
                  },
                  "to": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "from": "<generated pastdatetime>",
                "to": "<generated datetime>"
              }
            }
          }
        }
      }
    },
    "/api/analytics/datasets/{spec_id}/builds": {
      "get": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "List builds with lineage refs",
        "description": "Returns the reproducibility ledger for a dataset spec - every build with its time window and lineage_ref - scoped to the caller's tenant. Edge cases: an unknown spec_id or one owned by another tenant is NOT a 404 here; the query simply returns an empty array with 200. No pagination params are accepted, so specs with a long build history return the whole list.",
        "operationId": "get_api_analytics_datasets_spec_id_builds",
        "parameters": [
          {
            "name": "spec_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "spec_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "build_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <error message from listDatasetBuilds>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/analytics/datasets/{spec_id}/label-source": {
      "put": {
        "tags": [
          "sdk-analytics"
        ],
        "summary": "Set interval-based labels",
        "description": "Sets or replaces the labeling source on a dataset spec for supervised training: either kind='intervals' (inline labeled time ranges plus an optional default_label) or kind='provider' (a provider joined against events/evidence, configured via provider_args). Edge cases: kind is strictly validated and any other value (including omitting it) is 400; an unknown spec_id or one owned by another tenant is 404; intervals are not validated for overlap, ordering, or emptiness at the route; the PUT fully replaces the previous label source, so it is idempotent for a given body.",
        "operationId": "put_api_analytics_datasets_spec_id_label_source",
        "parameters": [
          {
            "name": "spec_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "spec_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "label_source_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "intervals",
                    "default_label": 0,
                    "intervals": [
                      {
                        "from": "2026-01-15T10:30:00Z",
                        "to": "2026-01-15T10:30:00Z",
                        "label": 1
                      }
                    ],
                    "provider_args": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — dataset spec not found"
          },
          "500": {
            "description": "InternalError — <error message from updateDatasetLabelSource>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "default_label": {
                    "type": "integer"
                  },
                  "intervals": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "from": {
                          "type": "string"
                        },
                        "to": {
                          "type": "string"
                        },
                        "label": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "provider_args": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "kind": "intervals",
                "default_label": 0,
                "intervals": [
                  {
                    "from": "<generated pastdatetime>",
                    "to": "<generated datetime>",
                    "label": 1
                  }
                ],
                "provider_args": {}
              }
            }
          }
        }
      }
    },
    "/api/api-keys": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Issue a key with crm + engagement scopes",
        "description": "Issues a new API key for a tenant with an explicit scope list and optional rate limit and expiry, returning 201 with the key record plus the one-time plaintext secret. Edge cases: tenant_id is required and scopes must be a non-empty array of strings — an empty array, a non-array, or an array containing a non-string all fail as \"scopes must be a non-empty string array\"; rate_limit_rpm, when supplied, must be a finite positive number so 0 and negatives are rejected; expires_at must parse as ISO-8601 and an unparseable value is a 400, but a past expiry is not rejected by the validator; all validation failures are accumulated and returned together in one details array; tenant_id comes from the body rather than the JWT, and a tenant_id with no matching tenant row fails at insert and surfaces as a 500.",
        "operationId": "post_api_api_keys",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "api_key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "scopes": [
                      "crm.contact.read",
                      "engagement.encounter.create"
                    ],
                    "rate_limit_rpm": 600,
                    "expires_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit_rpm": {
                    "type": "integer"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "scopes": [
                  "crm.contact.read",
                  "engagement.encounter.create"
                ],
                "rate_limit_rpm": 600,
                "expires_at": "<generated futuredatetime>"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "List keys for the tenant",
        "description": "Lists the API keys belonging to a tenant, identified by the required tenant_id query param, returning metadata only (the plaintext secret is never re-exposed after issue). Edge cases: tenant_id is mandatory and is trimmed, so a whitespace-only value is treated as missing and returns 400; a tenant with no keys returns 200 with an empty keys array rather than 404; tenant_id is read from the query string and not cross-checked against the JWT tenant_id, so scoping is by the supplied parameter — any authenticated caller who knows a tenant_id can enumerate that tenant's key metadata; the listing has no pagination or limit, so tenants with many keys return the full set in one response, and revoked/rotated keys remain in the listing with their status rather than being filtered out.",
        "operationId": "get_api_api_keys",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "api_key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/api-keys/{key_id}/revoke": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Revoke the freshly-issued key",
        "description": "Immediately revokes an API key by id so it stops authenticating, returning the updated key record. Edge cases: revokeKey only matches an ACTIVE key, so revoking a key that is already revoked returns 404 NotFound (\"No active key with id ...\") rather than a 200 no-op — the call is therefore not idempotent and a retry after a successful revoke will 404; an unknown key_id is likewise 404; a malformed (non-UUID) key_id fails the query cast and surfaces as a 500, since the route performs no id-format validation; there is no tenant ownership check against the JWT, so authorization rests on key ids being unguessable; revocation is immediate with no grace period, unlike rotate.",
        "operationId": "post_api_api_keys_key_id_revoke",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No active key with id <key_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/api-keys/{key_id}/rotate": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Rotate the freshly-issued key",
        "description": "Rotates an API key: mints a replacement secret and returns it with 201 while leaving the old key valid for a 24-hour grace window (FR-APK-4) so callers can cut over without downtime. Edge cases: only a rotatable (active, non-revoked) key matches, so rotating an already-revoked key returns 404 \"No rotatable key with id ...\"; an unknown key_id is also 404; rotation is not idempotent — each call mints a new secret and restarts the grace window, so a retried request produces a second replacement key rather than returning the first; the plaintext replacement secret appears only in this response and cannot be re-read afterwards; a malformed key_id fails the query cast and surfaces as 500.",
        "operationId": "post_api_api_keys_key_id_rotate",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "rotate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No rotatable key with id <key_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/app-identities/{app_identity_id}": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Get app_identity by id",
        "description": "Fetches one L2 app_identity row by its app_identity_id path param. Requires a valid tenant JWT (requireAuth). Edge cases: the lookup is by primary key only - the caller JWT tenant is never compared to the row, so any authenticated caller can read any app_identity and tenant scoping is not enforced here; a well-formed UUID that does not exist returns 404 NotFound, whereas a malformed non-UUID id fails the Postgres uuid cast inside an untried service call and surfaces as a Fastify 500, not a 400 or 404.",
        "operationId": "get_api_app_identities_app_identity_id",
        "parameters": [
          {
            "name": "app_identity_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "app_identity_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/app-identities/{app_identity_id}/memberships": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List memberships",
        "description": "Lists the L3 memberships attached to one app_identity. Requires a valid tenant JWT (requireAuth). Edge cases: this is a list, so an unknown but well-formed app_identity_id returns 200 with an empty memberships array - never a 404; there is no limit/offset paging, so an app_identity with many memberships returns them all in one payload; the caller JWT tenant is not compared to the rows, so membership rows for other tenants are visible to any authenticated caller - test tenant scoping explicitly; a malformed non-UUID app_identity_id fails the uuid cast in an untried service call and surfaces as a Fastify 500.",
        "operationId": "get_api_app_identities_app_identity_id_memberships",
        "parameters": [
          {
            "name": "app_identity_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "app_identity_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/app-identities": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Create app_identity for person + app",
        "description": "Creates an L2 app_identity binding a canonical person_id to an app_id, returning 201 with the new row. Requires a valid tenant JWT (requireAuth). Edge cases: only presence of person_id and app_id is validated - no UUID-format check, no existence check and no tenant check - so a non-existent person_id/app_id or a malformed UUID escapes validation and fails inside the service; the route has no try/catch, so that failure is answered by the Fastify default error handler as 500 rather than 404 or 400. Empty strings count as missing and yield 400. The caller JWT tenant is never consulted, so cross-tenant creation is not blocked here. The endpoint is not idempotent: repeating the same person_id+app_id either creates another row or trips a unique constraint (again a 500) depending on the schema.",
        "operationId": "post_api_app_identities",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "app_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/approvals/requests": {
      "get": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "List pending requests for tenant",
        "description": "Gateway-composed inbox of PENDING approval requests for a tenant, oldest-first. Optional ?assignee_persona_id= narrows it to requests having at least one undecided step assigned to that persona — note assignment lives on approval.step, not on the request. Edge cases: ?tenant_id= is REQUIRED (400 if absent) and is caller-asserted; only status='pending' rows are ever returned, so approved/rejected history is unreachable here; hard-capped at LIMIT 100 with no paging cursor; an assignee with nothing queued returns an empty array, not 404; a non-UUID tenant_id or assignee_persona_id fails its ::uuid cast and returns 500.",
        "operationId": "get_api_approvals_requests",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Submit refund approval request",
        "description": "Submits an approval request against an existing route, materialising the route steps into pending approval.step rows and returning the request plus its first step(s). Edge cases: tenant_id, route_id and initiator_persona_id must all be well-formed UUIDs; subject_kind and subject_id are required non-empty strings identifying what is being approved; a syntactically valid route_id matching no route row returns 404 RouteNotFound; the route's tenant is not cross-checked against the body tenant_id, so a mismatch is not rejected at this layer; there is no duplicate-request guard — submitting the same (subject_kind, subject_id) twice creates two independent requests.",
        "operationId": "post_api_approvals_requests",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_kind": "payment.refund",
                    "subject_id": "sample-slug",
                    "initiator_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "Customer dispute - high value refund",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID / route_id must be a UUID / subject_kind is required / subject_id is required / initiator_persona_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "RouteNotFound — Route <route_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "route_id": {
                    "type": "string"
                  },
                  "subject_kind": {
                    "type": "string"
                  },
                  "subject_id": {
                    "type": "string"
                  },
                  "initiator_persona_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "route_id": "<id from approvals>",
                "subject_kind": "payment.refund",
                "subject_id": "<generated slug>",
                "initiator_persona_id": "<id from auth>",
                "reason": "Customer dispute - high value refund"
              }
            }
          }
        }
      }
    },
    "/api/approvals/requests/{request_id}": {
      "get": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Get request state + steps",
        "description": "Reads one approval request with its steps and current status. Edge cases: an unknown :request_id returns 404 NotFound with the id echoed in details[]; the read is not tenant-filtered, so any authenticated caller holding a request_id can read it; a non-UUID :request_id reaches the query and is routed through the shared fail() mapper, which matches none of the typed errors and therefore returns 500 InternalError rather than 400.",
        "operationId": "get_api_approvals_requests_request_id",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Request <request_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/approvals/requests/{request_id}/decide": {
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Approve a pending request directly",
        "description": "Gateway request-level decision shortcut: stamps approval.request with status, final_decision, resolved_at and a reason of the form '[decided by <persona>] <comment>'. Distinct from the step-level decide route — it resolves the whole request in one write. Edge cases: decision, comment AND decider_persona_id are all mandatory (one 400 covers all three); the UPDATE is guarded by status='pending', so deciding an already-resolved request updates zero rows yet STILL returns {success:true} — the same holds for an unknown :request_id, so this endpoint can never report 404 or 409; decider_persona_id is only recorded in free text and is not checked against the step's assigned approver.",
        "operationId": "post_api_approvals_requests_request_id_decide",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decide_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approved",
                    "comment": "Approved after finance review",
                    "decider_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — decision + comment + decider_persona_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string"
                  },
                  "comment": {
                    "type": "string"
                  },
                  "decider_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "decision": "approved",
                "comment": "Approved after finance review",
                "decider_persona_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/approvals/routes": {
      "get": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "List routes for tenant",
        "description": "Gateway-composed list of a tenant's approval routes (route_id, name, status, created_at) with a derived sla_minutes = the MAX sla_minutes across the route's steps JSON, defaulting to 0 when no step declares one. Edge cases: ?tenant_id= is REQUIRED (400 if absent) and is caller-asserted from the query string rather than taken from the JWT; results are newest-first and hard-capped at LIMIT 100 with no paging cursor, so tenants with more than 100 routes are silently truncated; a tenant with no routes returns 200 with an empty data array; a tenant_id that is not a UUID fails the ::uuid cast and returns 500.",
        "operationId": "get_api_approvals_routes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Create 2-step approval route (manager single -> finance m-of-n)",
        "description": "Creates an approval route: a named, ordered chain of steps for a tenant, where each step is single (one approver_persona_id), m-of-n (m + approvers[]) or role (role_template_id). Optional kind_pattern binds the route to a subject kind and delegation_rules configure stand-ins. Edge cases: tenant_id must be a well-formed UUID (presence alone is not enough); steps must be a NON-EMPTY array and every element must carry a name plus a recognised kind with that kind's required field — one bad element rejects the whole payload; all validation failures are collected and returned together in details[]; an m-of-n step's m is not range-checked against approvers.length; delegation_rules is accepted only if it is an object and is otherwise silently dropped.",
        "operationId": "post_api_approvals_routes",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "high-value-refund",
                    "description": "Refunds above $10k",
                    "kind_pattern": "payment.refund > 10000 USD",
                    "steps": [
                      {
                        "name": "manager",
                        "kind": "single",
                        "approver_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                        "sla_minutes": 60
                      }
                    ],
                    "delegation_rules": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID / name is required / steps must be a non-empty array / each step needs {name, kind: single|m-of-n|role, ...}"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "kind_pattern": {
                    "type": "string"
                  },
                  "steps": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string"
                        },
                        "approver_persona_id": {
                          "type": "string"
                        },
                        "sla_minutes": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "delegation_rules": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "high-value-refund",
                "description": "Refunds above $10k",
                "kind_pattern": "payment.refund > 10000 USD",
                "steps": [
                  {
                    "name": "manager",
                    "kind": "single",
                    "approver_persona_id": "<id from auth>",
                    "sla_minutes": 60
                  }
                ],
                "delegation_rules": {}
              }
            }
          }
        }
      }
    },
    "/api/approvals/steps/{step_id}/decide": {
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Approve manager step",
        "description": "Records an approve/reject decision on one approval step and advances the request. Edge cases: :step_id must be a UUID and decision must be exactly 'approve' or 'reject' (anything else, including 'approved'/'rejected', is a 400); acting_persona_id must be a UUID and must MATCH the step's assigned approver — a different persona gets 403 NotYourStep, the key authorisation case here; a step that already carries a decision returns 409 StepAlreadyDecided, so this endpoint is explicitly NOT idempotent on retry; an unknown step_id returns 404 StepNotFound; reason is optional even on reject.",
        "operationId": "post_api_approvals_steps_step_id_decide",
        "parameters": [
          {
            "name": "step_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "step_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decide_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approve",
                    "acting_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "LGTM after review",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — step_id path param must be a UUID / decision must be 'approve' or 'reject' / acting_persona_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "NotYourStep — Step is assigned to <approver_persona_id>, not <acting_persona_id>"
          },
          "404": {
            "description": "StepNotFound — Step <step_id> not found"
          },
          "409": {
            "description": "StepAlreadyDecided — Step <step_id> already decided"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decision": {
                    "type": "string"
                  },
                  "acting_persona_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "decision": "approve",
                "acting_persona_id": "<id from auth>",
                "reason": "LGTM after review"
              }
            }
          }
        }
      }
    },
    "/api/assets/{asset_id}/commands": {
      "get": {
        "tags": [
          "sdk-asset"
        ],
        "summary": "List commands for an asset",
        "description": "Lists the commands issued against one asset, scoped to the caller's tenant taken from the JWT tenant_id claim, and returns 200 with the array. Because the tenant comes from the token, a caller cannot read another tenant's command history through this route. Edge cases: a token with no tenant_id claim is a 400; an asset_id belonging to a different tenant, or one with no commands yet, returns 200 with an empty array rather than a 403 or 404; the listing is unpaginated, so an asset with a long command history returns every row; a non-UUID asset_id fails the Postgres UUID cast and is reported as a generic 500; requires a valid JWT.",
        "operationId": "get_api_assets_asset_id_commands",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "command_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant context required — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "<underlying error message> — Command listing failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assets/{asset_id}/credentials": {
      "post": {
        "tags": [
          "sdk-asset"
        ],
        "summary": "Mint a credential scoped to the robot",
        "description": "Mints a per-robot scoped API credential for the asset and returns 201 with the plaintext key exactly once - it is never retrievable again, so the edge agent must persist it on receipt. The credential is scoped to command-ack plus this asset's delivery stream. The tenant comes solely from the JWT tenant_id claim (there is no body override), so a token with no tenant claim is a 400. Both body fields are optional: rate_limit_rpm and expires_at fall back to service defaults. Edge cases: repeated calls mint additional independent credentials rather than rotating or conflicting; an expires_at in the past is not validated here; an unknown asset_id is not pre-checked and surfaces as a generic 500 if the underlying insert fails; requires a valid JWT.",
        "operationId": "post_api_assets_asset_id_credentials",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "credential_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rate_limit_rpm": 600,
                    "expires_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant context required — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "<underlying error message> — Credential issuance failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rate_limit_rpm": {
                    "type": "integer"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "rate_limit_rpm": 600,
                "expires_at": "<generated futuredatetime>"
              }
            }
          }
        }
      }
    },
    "/api/assets/{asset_id}/readings": {
      "get": {
        "tags": [
          "sdk-asset"
        ],
        "summary": "Query minute-bucketed readings for an asset",
        "description": "Queries the sensor time-series for one asset, either raw or aggregated, filtered by the optional sensor_id, from and to query parameters. The optional bucket parameter selects a rollup grain and is validated against the closed set second | minute | hour | day - an unrecognised bucket is silently ignored and the query falls back to raw readings rather than erroring. Edge cases: an unknown asset_id or an empty time window returns 200 with an empty result set, not a 404; from/to are passed through to the storage layer, so an unparseable timestamp surfaces as a generic 500; an inverted range (from after to) yields an empty set; no tenant check is performed against the JWT; requires a valid JWT.",
        "operationId": "get_api_assets_asset_id_readings",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reading_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "<underlying error message> — Reading query failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assets/{asset_id}/twin": {
      "get": {
        "tags": [
          "sdk-asset"
        ],
        "summary": "Read the full nested twin",
        "description": "Returns the full digital twin for one asset - the asset row plus its component tree and sensor definitions - with status 200. Read-only. The handler scopes by asset_id only and never compares the twin's tenant against the JWT, so it is not tenant-isolated. Edge cases: an unknown asset_id returns 404; an asset registered with no components returns 200 with an empty component tree rather than 404; a non-UUID asset_id fails the Postgres UUID cast inside the try block and is reported as a generic 500 rather than a 400; requires a valid JWT.",
        "operationId": "get_api_assets_asset_id_twin",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "twin_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "asset not found — asset not found"
          },
          "500": {
            "description": "<underlying error message> — Twin lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assets": {
      "post": {
        "tags": [
          "sdk-asset"
        ],
        "summary": "Register a humanoid with a component/sensor tree",
        "description": "Registers a digital-twin asset (a robot) together with its component tree and sensors, returning 201 with the created registry entry. The tenant is taken from the body when supplied and otherwise falls back to the JWT tenant_id claim, so a caller with a tenant-scoped token may omit it - but a token with no tenant_id claim and no body tenant_id is a 400. components defaults to an empty array, so an asset can be registered with no component tree. Edge cases: every downstream failure (duplicate device_uuid, malformed component tree, database error) is collapsed into a single generic 500 with the raw error message - there is no 409 path; model and display_name are unvalidated free text; requires a valid JWT.",
        "operationId": "post_api_assets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "asset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "bu_id": "{{var:bu_id}}",
                    "device_uuid": "dev_robotA",
                    "model": "Humanoid-X1",
                    "display_name": "Unit-001",
                    "components": [
                      {
                        "kind": "head",
                        "name": "head",
                        "position": {
                          "x": 0,
                          "y": 0,
                          "z": 1
                        },
                        "sensors": [
                          {
                            "kind": "camera",
                            "unit": "frame",
                            "min_value": 0,
                            "max_value": 60,
                            "sample_rate_hz": 30
                          }
                        ]
                      },
                      {
                        "kind": "arm",
                        "name": "left_arm",
                        "children": [
                          {
                            "kind": "hand",
                            "children": [
                              {
                                "kind": "finger",
                                "sensors": [
                                  {
                                    "kind": "force",
                                    "unit": "N",
                                    "min_value": 0,
                                    "max_value": 100,
                                    "sample_rate_hz": 200
                                  }
                                ]
                              }
                            ]
                          }
                        ]
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant_id required — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "<underlying error message> — Asset registration failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "bu_id": {
                    "type": "string"
                  },
                  "device_uuid": {
                    "type": "string"
                  },
                  "model": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "components": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "position": {
                          "type": "object",
                          "properties": {
                            "x": {
                              "type": "integer"
                            },
                            "y": {
                              "type": "integer"
                            },
                            "z": {
                              "type": "integer"
                            }
                          }
                        },
                        "sensors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "kind": {
                                "type": "string"
                              },
                              "unit": {
                                "type": "string"
                              },
                              "min_value": {
                                "type": "integer"
                              },
                              "max_value": {
                                "type": "integer"
                              },
                              "sample_rate_hz": {
                                "type": "integer"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "bu_id": "{{var:bu_id}}",
                "device_uuid": "dev_robotA",
                "model": "Humanoid-X1",
                "display_name": "Unit-001",
                "components": [
                  {
                    "kind": "head",
                    "name": "head",
                    "position": {
                      "x": 0,
                      "y": 0,
                      "z": 1
                    },
                    "sensors": [
                      {
                        "kind": "camera",
                        "unit": "frame",
                        "min_value": 0,
                        "max_value": 60,
                        "sample_rate_hz": 30
                      }
                    ]
                  },
                  {
                    "kind": "arm",
                    "name": "left_arm",
                    "children": [
                      {
                        "kind": "hand",
                        "children": [
                          {
                            "kind": "finger",
                            "sensors": [
                              {
                                "kind": "force",
                                "unit": "N",
                                "min_value": 0,
                                "max_value": 100,
                                "sample_rate_hz": 200
                              }
                            ]
                          }
                        ]
                      }
                    ]
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/assignment/assign-by-task": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Round-robin assign among three eligible, skilled personas",
        "description": "Auto-assign a task to the best-fit persona. The skill, availability-window and per-day capacity gates always run first; then the chosen strategy selects the winner. strategy='default' (or omitted) keeps the primary->backup + distance + capacity scoring; strategy='round_robin' cycles fairly through eligible candidates via a per-(tenant,pool,strategy) rotation cursor advanced atomically (concurrency-safe, no over-assignment); strategy='fair_share' biases toward the least-loaded candidate, rotating among ties. Returns the proposed assignment (status='proposed') plus the matched territory, distance and a reason string. The three candidate personas are seeded into assignment.workload by the setupScript (workload has no HTTP CRUD and no FK/tenant column). 409 when no eligible persona (empty pool / all at capacity).",
        "operationId": "post_api_assignment_assign_by_task",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "assign_by_task_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "task_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "location": {
                      "lat": 40.7128,
                      "lng": -74.006
                    },
                    "required_skills": [
                      "plumbing"
                    ],
                    "strategy": "round_robin",
                    "pool_key": "default",
                    "candidate_persona_ids": [
                      "a1111111-1111-4111-8111-111111111111",
                      "a2222222-2222-4222-8222-222222222222",
                      "a3333333-3333-4333-8333-333333333333"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — task_id and tenant_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "409": {
            "description": "NoEligiblePersona — no eligible persona for task"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "task_id": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "location": {
                    "type": "object",
                    "properties": {
                      "lat": {
                        "type": "number"
                      },
                      "lng": {
                        "type": "number"
                      }
                    }
                  },
                  "required_skills": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "strategy": {
                    "type": "string"
                  },
                  "pool_key": {
                    "type": "string"
                  },
                  "candidate_persona_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "task_id": "<generated uuid>",
                "tenant_id": "<id from auth>",
                "location": {
                  "lat": 40.7128,
                  "lng": -74.006
                },
                "required_skills": [
                  "plumbing"
                ],
                "strategy": "round_robin",
                "pool_key": "default",
                "candidate_persona_ids": [
                  "a1111111-1111-4111-8111-111111111111",
                  "a2222222-2222-4222-8222-222222222222",
                  "a3333333-3333-4333-8333-333333333333"
                ]
              }
            }
          }
        }
      }
    },
    "/api/audit/append": {
      "post": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "Append entry to canonical audit chain",
        "description": "Appends a hash-chained immutable entry to a per-pool audit chain, updating audit.chain_head in the same transaction; returns 201 with the new entry id/seq/hashes. actor_id comes from the JWT subject (defaults \"unknown\"); actor_kind defaults \"human\". Edge cases: missing pool_index/event_type/payload, actor_kind outside [human,service,agent], retention_class outside [transient,operational,regulated], and an event_type not in the canonical EVENT_TYPE_REGISTRY (rejected 400 before any write).",
        "operationId": "post_api_audit_append",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "append_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "pool_index": "app-healthcare-007",
                    "event_type": "identity.login.v1",
                    "payload": {
                      "action": "create",
                      "resource": "Acme QA Sample"
                    },
                    "actor_kind": "human",
                    "retention_class": "operational",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "bu_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_kind": "user",
                    "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — details[]: pool_index/event_type/payload required, actor_kind must be one of human,service,agent, retention_class must be one of transient,operational,regulated, or body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pool_index": {
                    "type": "string"
                  },
                  "event_type": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "action": {
                        "type": "string"
                      },
                      "resource": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_kind": {
                    "type": "string"
                  },
                  "retention_class": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "org_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "bu_id": {
                    "type": "string"
                  },
                  "subject_kind": {
                    "type": "string"
                  },
                  "subject_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "pool_index": "app-healthcare-007",
                "event_type": "identity.login.v1",
                "payload": {
                  "action": "create",
                  "resource": "<generated name>"
                },
                "actor_kind": "human",
                "retention_class": "operational",
                "tenant_id": "<id from auth>",
                "org_id": "<id from auth>",
                "app_id": "<id from auth>",
                "bu_id": "<id from tenants>",
                "subject_kind": "user",
                "subject_id": "<id from scim>"
              }
            }
          }
        }
      }
    },
    "/api/audit/export": {
      "post": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "Request an inline JSONL export of the tenant audit chain",
        "description": "Customer-facing self-audit export (FR-AUD-4): a tenant requests a signed PDF or JSONL dump of its own audit chain over an ISO-8601 date range. By default the request is only queued and returns 202 with a pending request_id; passing inline=true materializes the artifact synchronously and returns 201 with artifact_s3_key and signature_hex. QA edge cases: the handler collects ALL validation failures into one 400 (missing tenant_id, format outside {pdf,jsonl}, missing/unparseable range_start or range_end, and range_start > range_end are reported together in details[]); an empty body {} yields four validation messages at once. Dates are parsed with new Date(), so garbage strings ('not-a-date') fail as NaN while loose forms ('2026-01-01') are accepted. format defaults to 'jsonl' when omitted, and inline is only honoured when strictly boolean true (the string \"true\" queues instead of materializing). tenant_id is taken from the BODY and is not cross-checked against the caller's JWT, so tenant-scoping tests must assert on the returned request_id rather than on rejection. The endpoint is not idempotent — repeating the same range creates a new request_id every call. A zero-width range (range_start == range_end) is valid and exports an empty chain. No pagination; oversized ranges are accepted and produce a larger artifact. As a non-public gateway path it is behind the default-deny auth gate, so a missing/expired bearer token is rejected before the handler runs.",
        "operationId": "post_api_audit_export",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "export_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "format": "jsonl",
                    "range_start": "2026-01-15T10:30:00Z",
                    "range_end": "2026-01-15T10:30:00Z",
                    "inline": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "format": {
                    "type": "string"
                  },
                  "range_start": {
                    "type": "string"
                  },
                  "range_end": {
                    "type": "string"
                  },
                  "inline": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "format": "jsonl",
                "range_start": "<generated pastdatetime>",
                "range_end": "<generated futuredatetime>",
                "inline": true
              }
            }
          }
        }
      }
    },
    "/api/audit/verify": {
      "post": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "Verify chain for an empty pool",
        "description": "On-demand hash-chain verification for one audit pool (P1-Foundation-Spine §7): walks audit.entry from from_seq to to_seq inclusive, recomputes each entry_hash from the canonical serialization, checks every prev_hash links to the preceding entry_hash, and returns the canonical proof object. QA edge cases: the status code is data-dependent — an intact chain returns 200 while a detected break returns 409 with the SAME body shape (data.ok=false, data.break_at_seq, data.break_reason of either 'prev_hash does not match the previous entry_hash' or 'entry_hash mismatch — payload tampered or canonicalization drift'), so tests must assert on data.ok, not only on status. An unknown pool_index or a from_seq/to_seq window containing no rows is NOT an error: it returns 200 with entries_checked=0, ok=true and head_hash_hex=null, so a missing-FK style negative test cannot be written here. pool_index is trimmed and a whitespace-only value is treated as empty (400). from_seq/to_seq are only honoured when typeof === 'number' — string \"1\" is silently ignored and the full pool is scanned; an inverted or out-of-range window simply yields zero rows. The call is read-only and fully idempotent apart from stamping audit.chain_head.last_verified_at on success. There is no pagination or row cap, so a large window scans the whole range in one request. pool_index is caller-supplied and not scoped to the JWT tenant, but the path sits behind the gateway default-deny gate so an invalid or expired token is rejected before the handler runs.",
        "operationId": "post_api_audit_verify",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "pool_index": "app-healthcare-007",
                    "from_seq": 1,
                    "to_seq": 100,
                    "verify_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — pool_index is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "ChainBreak — data.ok=false with break_at_seq and break_reason set"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pool_index": {
                    "type": "string"
                  },
                  "from_seq": {
                    "type": "integer"
                  },
                  "to_seq": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "pool_index": "app-healthcare-007",
                "from_seq": 1,
                "to_seq": 100
              }
            }
          }
        }
      }
    },
    "/api/auth/login": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Login with previously registered credentials",
        "description": "Verifies an email + password credential and mints a six-layer JWT. Public - the gateway authGate allowlists /api/auth/login, so no bearer token is needed. An optional tenant_id selects the tenant context from the person memberships, and an optional app_id auto-mints the L2 AppIdentity on first per-app login. Edge cases: only presence of email and password is validated (no format or length rule), so a blank value is a 400 but a syntactically invalid email reaches the credential check and returns 401; a wrong password and an unknown email return the same 401 InvalidCredentials, so there is no user enumeration; a valid credential plus a tenant_id the person has no active membership in is a 403, distinct from the 401; omitting tenant_id yields a token with tenant_id null and skips both the membership check and the AppIdentity mint; passing app_id without tenant_id silently skips the mint.",
        "operationId": "post_api_auth_login",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "login_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "email": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "password": "DefaultTestPass123!",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — email is required / password is required"
          },
          "401": {
            "description": "InvalidCredentials — Invalid email or password"
          },
          "403": {
            "description": "NoMembership — Person <person_id> has no active membership in tenant <tenant_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "email": "<id from auth>",
                "password": "{{static:DefaultTestPass123!}}"
              }
            }
          }
        }
      }
    },
    "/api/auth/register": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Register new user with email, password, name and phone",
        "description": "Creates a canonical identity.person plus an email alias and a password credential, then returns 201 with the person id and a freshly minted six-layer JWT that carries no tenant scope. Public - the gateway authGate allowlists /api/auth/register. Edge cases: email must match the email regex and password must be at least 8 characters, so a 7-character password is a 400; given_name, family_name and display_name are each capped at 120 characters and an oversized value is a 400; phone is optional but regex-checked when present; re-registering an email that already exists is a 409 UserExists, not a 200 - the endpoint is not idempotent; validation accumulates, so one 400 may list several messages in details[].",
        "operationId": "post_api_auth_register",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "register_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "email": "qa.user@example.com",
                    "password": "DefaultTestPass123!",
                    "given_name": "Acme QA Sample",
                    "family_name": "Acme QA Sample",
                    "display_name": "Acme QA Sample",
                    "phone": "+15555550123",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — email is required / email is invalid / password is required / password must be at least 8 characters / given_name|family_name|display_name must be 120 characters or fewer / phone is invalid"
          },
          "409": {
            "description": "UserExists — A person with this email already exists"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  },
                  "given_name": {
                    "type": "string"
                  },
                  "family_name": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "email": "<generated email>",
                "password": "{{static:DefaultTestPass123!}}",
                "given_name": "<generated name>",
                "family_name": "<generated name>",
                "display_name": "<generated name>",
                "phone": "<generated phone>"
              }
            }
          }
        }
      }
    },
    "/api/auth/signup-tenant": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Self-serve signup with company, founder name and phone",
        "description": "Self-serve onboarding: creates the person, their org, a default app, a trial tenant and the admin membership in one transaction, then returns 201 with a JWT already scoped to the new tenant and app. Public - the gateway authGate allowlists /api/auth/signup-tenant. Edge cases: email format and an 8-character minimum password are enforced, company_name is required and capped at 80 characters, and given_name/family_name/display_name are each capped at 120 - each violation is a 400 and details[] may list several at once; signing up with an email that already has a person is a 409 UserExists and the whole org/app/tenant transaction rolls back, so no partial tenant is left behind; the endpoint is not idempotent - retrying with a fresh email creates a second org and tenant.",
        "operationId": "post_api_auth_signup_tenant",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "signup_tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "email": "qa.user@example.com",
                    "password": "DefaultTestPass123!",
                    "company_name": "Acme QA Sample",
                    "region": "us-east-1",
                    "given_name": "Acme QA Sample",
                    "family_name": "Acme QA Sample",
                    "display_name": "Acme QA Sample",
                    "phone": "+15555550123",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — email is required / email is invalid / password is required / password must be at least 8 characters / company_name is required / company_name must be 80 characters or fewer / given_name|family_name|display_name must be 120 characters or fewer / phone is invalid"
          },
          "409": {
            "description": "UserExists — A person with this email already exists"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  },
                  "company_name": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "given_name": {
                    "type": "string"
                  },
                  "family_name": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "email": "<generated email>",
                "password": "{{static:DefaultTestPass123!}}",
                "company_name": "<generated name>",
                "region": "{{static:us-east-1}}",
                "given_name": "<generated name>",
                "family_name": "<generated name>",
                "display_name": "<generated name>",
                "phone": "<generated phone>"
              }
            }
          }
        }
      }
    },
    "/api/billing/invoices/generate": {
      "post": {
        "tags": [
          "sdk-billing"
        ],
        "summary": "Generate Jan 2026 invoice",
        "description": "Generates an invoice for the caller's tenant over a closed billing period using a named rate catalog, returning 201 with the invoice and its line items. tenant_id is always taken from the verified JWT and never from the body, because invoice generation exposes financial PII. Edge cases: catalog_id is required and period_start/period_end must both be YYYY-MM-DD; period_start must be <= period_end so an inverted range is a 400; all validation failures are accumulated into one details array; a catalog_id that does not resolve raises CatalogNotFoundError and returns 404; generating for a period with no metered usage yields a zero-total invoice rather than an error; there is no idempotency key, so repeating the call for the same tenant/period/catalog produces another invoice.",
        "operationId": "post_api_billing_invoices_generate",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "catalog_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "period_start": "2026-01-01",
                    "period_end": "2026-01-31",
                    "currency": "USD",
                    "tax_rate": 0.0875,
                    "usage": [
                      {
                        "sku": "api.call",
                        "app_id": "admin",
                        "bu_id": "finance",
                        "persona_kind": "human",
                        "encounter_id": null,
                        "actor_kind": "human",
                        "units": 12000,
                        "vendor_cost": 0
                      }
                    ],
                    "generate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "CatalogNotFound — Rate catalog not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "catalog_id": {
                    "type": "string"
                  },
                  "period_start": {
                    "type": "string"
                  },
                  "period_end": {
                    "type": "string"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "tax_rate": {
                    "type": "number"
                  },
                  "usage": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "sku": {
                          "type": "string"
                        },
                        "app_id": {
                          "type": "string"
                        },
                        "bu_id": {
                          "type": "string"
                        },
                        "persona_kind": {
                          "type": "string"
                        },
                        "encounter_id": {
                          "nullable": true
                        },
                        "actor_kind": {
                          "type": "string"
                        },
                        "units": {
                          "type": "integer"
                        },
                        "vendor_cost": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "catalog_id": "<id from meter-pricing-catalogs>",
                "period_start": "2026-01-01",
                "period_end": "2026-01-31",
                "currency": "USD",
                "tax_rate": 0.0875,
                "usage": [
                  {
                    "sku": "api.call",
                    "app_id": "admin",
                    "bu_id": "finance",
                    "persona_kind": "human",
                    "encounter_id": null,
                    "actor_kind": "human",
                    "units": 12000,
                    "vendor_cost": 0
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/billing/live": {
      "get": {
        "tags": [
          "sdk-billing"
        ],
        "summary": "Read live meter for tenant",
        "description": "Returns the caller's current live meter reading — in-flight usage counters for the tenant that have not yet been rolled into an invoice. tenant_id comes exclusively from the verified JWT, so the endpoint always reports the caller's own tenant and cannot be pointed at another tenant via the query string. Edge cases: the only validation is that the JWT tenant_id is a well-formed UUID, so all other query params are ignored rather than rejected; a tenant with no recorded usage returns 200 with zeroed/empty counters rather than 404; values are point-in-time and unsmoothed, so consecutive calls can differ as the meter collector flushes; a token missing the tenant_id claim is 403 before any read.",
        "operationId": "get_api_billing_live",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "live_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "CatalogNotFound — Rate catalog not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/billing/reprice-dry-run": {
      "post": {
        "tags": [
          "sdk-billing"
        ],
        "summary": "Compare current catalog against itself (baseline == target dry-run)",
        "description": "Simulates repricing the caller's tenant over a period by replaying recorded usage against a target rate catalog and comparing it to a baseline catalog, returning 201 with the delta — no invoice is written and no state changes. tenant_id is forced from the verified JWT. Edge cases: baseline_catalog_id and target_catalog_id are both required and may legitimately be the same value, which yields a zero delta; period_start and period_end must both be YYYY-MM-DD; unlike invoice generation this validator does not enforce period_start <= period_end, so an inverted range passes validation and simply produces an empty usage window; either catalog id failing to resolve raises CatalogNotFoundError and returns 404; being a dry run the call is fully idempotent and repeatable.",
        "operationId": "post_api_billing_reprice_dry_run",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reprice_dry_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "period_start": "2026-01-01",
                    "period_end": "2026-01-31",
                    "baseline_catalog_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_catalog_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "usage": [
                      {
                        "sku": "api.call",
                        "app_id": "admin",
                        "bu_id": "finance",
                        "persona_kind": "human",
                        "encounter_id": null,
                        "actor_kind": "service",
                        "units": 50000,
                        "vendor_cost": 0
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "CatalogNotFound — Rate catalog not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "period_start": {
                    "type": "string"
                  },
                  "period_end": {
                    "type": "string"
                  },
                  "baseline_catalog_id": {
                    "type": "string"
                  },
                  "target_catalog_id": {
                    "type": "string"
                  },
                  "usage": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "sku": {
                          "type": "string"
                        },
                        "app_id": {
                          "type": "string"
                        },
                        "bu_id": {
                          "type": "string"
                        },
                        "persona_kind": {
                          "type": "string"
                        },
                        "encounter_id": {
                          "nullable": true
                        },
                        "actor_kind": {
                          "type": "string"
                        },
                        "units": {
                          "type": "integer"
                        },
                        "vendor_cost": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "period_start": "2026-01-01",
                "period_end": "2026-01-31",
                "baseline_catalog_id": "<id from meter-pricing-catalogs>",
                "target_catalog_id": "<id from meter-pricing-catalogs>",
                "usage": [
                  {
                    "sku": "api.call",
                    "app_id": "admin",
                    "bu_id": "finance",
                    "persona_kind": "human",
                    "encounter_id": null,
                    "actor_kind": "service",
                    "units": 50000,
                    "vendor_cost": 0
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/billing/showback": {
      "get": {
        "tags": [
          "sdk-billing"
        ],
        "summary": "Split by app_id + bu_id for Jan 2026",
        "description": "Produces a showback/chargeback breakdown of the caller's tenant usage across a period, optionally grouped by dimensions such as app_id and bu_id via the group_by query param. tenant_id is always taken from the verified JWT so one tenant can never pull another tenant's cost allocation. Edge cases: period_start and period_end are both required in YYYY-MM-DD form; the validator does not enforce start <= end, so an inverted range returns an empty breakdown rather than a 400; an unrecognised group_by dimension is not rejected at validation and either collapses the grouping or surfaces from the aggregation as a 500; a period with no usage returns 200 with empty groups rather than 404; results are computed on demand and unpaginated, so wide periods with many groups return a large single payload.",
        "operationId": "get_api_billing_showback",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "showback_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "CatalogNotFound — Rate catalog not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/break-glass/{grant_id}/decide": {
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Approve the gating request, activating the grant",
        "description": "Records an approve or reject decision on one step of a pending break-glass grant and returns the updated grant, which becomes active once all required steps approve. The deciding persona is taken from the JWT (primary_persona_id, else sub). Edge cases: step_id is mandatory and decision must be exactly \"approve\" or \"reject\" — any other value, including \"approved\" or a boolean, is a 400 at the route; an unknown grant_id returns 400 BreakGlass \"grant ... not found\", not 404, because the service signals it as a domain error; deciding on a grant that is no longer pending (already approved, rejected, expired or used) returns 400 BreakGlass \"grant ... is <status>, not pending\", so a duplicate decision is rejected rather than silently accepted — the call is not idempotent; reason is optional and is recorded for audit.",
        "operationId": "post_api_break_glass_grant_id_decide",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decide_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "step_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approve",
                    "reason": "Emergency verified - access approved",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — step_id and decision (approve|reject) are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "step_id": {
                    "type": "string"
                  },
                  "decision": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "step_id": "<id from break-glass>",
                "decision": "approve",
                "reason": "Emergency verified - access approved"
              }
            }
          }
        }
      }
    },
    "/api/break-glass/{grant_id}": {
      "get": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Read grant status and latest certificate",
        "description": "Reads a break-glass grant by id, returning its current status (pending / active / expired / consumed / rejected), its scope and justification, its approval steps and the latest issued certificate. Edge cases: unlike the decide and use routes — which report a missing grant as a domain error with 400/403 — this read returns a plain 404 NotFound for an unknown grant_id; it performs no tenant ownership check against the JWT, so any authenticated caller holding a valid grant_id can read that grant; a malformed (non-UUID) grant_id fails the query cast and surfaces as 500 rather than 404; the response reflects expiry as recorded status, so a grant whose TTL has lapsed may still read as active until it is evaluated at use time.",
        "operationId": "get_api_break_glass_grant_id",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "break_glass_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/break-glass": {
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Request emergency break-glass access to a patient record",
        "description": "Opens a scoped, approval-gated emergency access (break-glass) request against an approval route, returning 201 with the grant in pending status plus the approval steps it must clear. The requester persona is derived from the JWT (primary_persona_id, falling back to sub) and tenant_id defaults to the JWT tenant_id when the body omits it. Edge cases: route_id, a resolvable tenant_id and justification are all mandatory and produce a single combined 400; justification is re-checked inside the service and an empty one surfaces as a 400 BreakGlass error; ttl_minutes is optional and defaults in the service, bounding how long the grant stays usable once approved; scope defaults to an empty object, which grants nothing usable later since use-time action checks are scope-matched; a route_id that resolves to no approval route fails during grant creation and returns 400 BreakGlass \"failed to create break-glass grant\"; there is no idempotency key, so repeat submissions open additional pending grants.",
        "operationId": "post_api_break_glass",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "break_glass_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "justification": "Emergency access required to treat unconscious patient",
                    "scope": {
                      "resource": "patient.record",
                      "actions": [
                        "read"
                      ]
                    },
                    "ttl_minutes": 60,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — route_id, tenant_id, justification are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "route_id": {
                    "type": "string"
                  },
                  "justification": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "object",
                    "properties": {
                      "resource": {
                        "type": "string"
                      },
                      "actions": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "ttl_minutes": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "route_id": "<id from approvals>",
                "justification": "Emergency access required to treat unconscious patient",
                "scope": {
                  "resource": "patient.record",
                  "actions": [
                    "read"
                  ]
                },
                "ttl_minutes": 60
              }
            }
          }
        }
      }
    },
    "/api/break-glass/{grant_id}/use": {
      "post": {
        "tags": [
          "sdk-approval"
        ],
        "summary": "Exercise the active grant within scope, emitting a certificate",
        "description": "Exercises an approved break-glass grant to perform one emergency action against an optional target, returning an audit certificate that records what was done, by which persona and when. Edge cases: action is mandatory at the route; every failure from the service is surfaced as 403 (not 400) because a refusal here is an authorization denial — an unknown grant_id, a grant that is still pending or already consumed (\"is <status>, not active\"), a grant whose TTL has elapsed (\"has expired\"), and an action that falls outside the grant's recorded scope all return 403 with the specific reason; the acting persona is taken from the JWT rather than the body so a caller cannot attribute the use to someone else; target_id is optional, so scope checks that depend on a target must encode that in the scope object.",
        "operationId": "post_api_break_glass_grant_id_use",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "use_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "action": "read",
                    "target_id": "{{var:target_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — action is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "BreakGlass — grant <grant_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string"
                  },
                  "target_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "action": "read",
                "target_id": "{{var:target_id}}"
              }
            }
          }
        }
      }
    },
    "/bridge": {
      "get": {
        "tags": [
          "semantic-service"
        ],
        "summary": "List all registered cross-domain bridges",
        "description": "Lists every registered cross-domain bridge (semantic.cross_domain_bridge) as {success:true, data:[...]} with bridge_id, from_object_type_id, to_object_type_id, access_mode and requires_cross_tenant_consent. QA edge cases: the handler takes no query parameters — it is a global, unfiltered, unpaginated read, so there is no tenant scoping, no limit/offset and no ordering guarantee; a fresh database legitimately returns 200 with an empty data array rather than a 404. It is a pure read (idempotent, safe to re-run) and there are no validation branches, so the only client-side failure is authentication: /bridge is not on the gateway public allowlist, so the default-deny auth gate requires a valid tenant JWT. Because the route has no try/catch, a datastore failure surfaces as Fastify's default 500 envelope ({statusCode, error:'Internal Server Error', message}) rather than the {success:false} shape used by the write endpoints.",
        "operationId": "get_bridge",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "bridge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Register a cross-domain bridge between two object types",
        "description": "Registers a cross-domain bridge between two semantic object types, returning 200 {success:true, data:{bridge_id, from_object_type_id, to_object_type_id, access_mode, requires_cross_tenant_consent}} and emitting a semantic.bridge.created.v1 audit entry. QA edge cases: the INSERT is ON CONFLICT (from_object_type_id, to_object_type_id) DO UPDATE, so re-posting the same pair is idempotent on identity — it returns the SAME bridge_id (never 409) but overwrites access_mode and requires_cross_tenant_consent, which is the way to test duplicate handling. Only from_object_type_id and to_object_type_id are checked for presence; access_mode defaults to 'read-only' and requires_cross_tenant_consent defaults to true when omitted. Everything past that check is wrapped in one catch that maps ANY thrown error to 400, so missing-FK cases (an object_type_id that does not exist), malformed UUIDs, an access_mode outside {read-only, read-write} rejected by the column constraint, and a self-referencing bridge all surface as 400 with the raw Postgres message in `error` — never 404 and never 500. Empty-string ids are falsy and hit the required-fields branch instead. The bridge is global (no tenant_id column), so it is not tenant-scoped; the path is not on the gateway public allowlist, so a valid tenant JWT is required.",
        "operationId": "post_bridge",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "bridge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "from_object_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to_object_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "access_mode": "read-only",
                    "requires_cross_tenant_consent": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — from_object_type_id and to_object_type_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from_object_type_id": {
                    "type": "string"
                  },
                  "to_object_type_id": {
                    "type": "string"
                  },
                  "access_mode": {
                    "type": "string"
                  },
                  "requires_cross_tenant_consent": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "from_object_type_id": "<id from ontology>",
                "to_object_type_id": "<id from ontology>",
                "access_mode": "read-only",
                "requires_cross_tenant_consent": true
              }
            }
          }
        }
      }
    },
    "/api/build/plan": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Compose a plan for a financial accounting app",
        "description": "Planner v2: retrieve-then-compose. Embeds the intent locally (bge-small), retrieves top-K candidate SDKs, injects the foundation/AIM tier + dependency closure, then composes a plan from candidates only. Returns { plan: { summary, recommended_sdks[], custom_work[], clarifying_questions[], vertical_pack, complexity }, meta: { catalog_size, provider, audit_status, retrieval } }.",
        "operationId": "post_api_build_plan",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "intent": "A financial accounting system with invoices, AR/AP and monthly close",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "IntentRequired — intent is required"
          },
          "401": {
            "description": "unauthorized — unauthorized"
          },
          "422": {
            "description": "NoCandidateSdks — no candidate SDKs matched the intent; try describing the app differently"
          },
          "500": {
            "description": "CatalogLoadFailed — failed to load SDK catalog: <underlying error>"
          },
          "502": {
            "description": "PlanComposeFailed — <LLM provider error message>"
          },
          "503": {
            "description": "LlmProviderUnavailable — No LLM provider configured / <PROVIDER>_API_KEY is not set"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "intent": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "intent": "A financial accounting system with invoices, AR/AP and monthly close"
              }
            }
          }
        }
      }
    },
    "/api/campaigns/{campaign_id}/journeys": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Define a two-step journey (delay then notification)",
        "description": "Creates a journey (an ordered step list stored as opaque jsonb) under a campaign. The handler does no body validation: an omitted steps array defaults to [], and step contents are never schema-checked here - downstream workers interpret them. Edge cases: an unknown or non-UUID campaign_id fails the FK/cast in the insert and surfaces as an unhandled 500, not a 404; a zero-step journey is accepted and any run against it completes on the first advance; each call inserts a new journey row, so this is not idempotent.",
        "operationId": "post_api_campaigns_campaign_id_journeys",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "campaign_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "journey_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "steps": [
                      {
                        "kind": "delay",
                        "duration_hours": 24
                      },
                      {
                        "kind": "notification",
                        "template_code": "welcome_v1"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "steps": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "duration_hours": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "steps": [
                  {
                    "kind": "delay",
                    "duration_hours": 24
                  },
                  {
                    "kind": "notification",
                    "template_code": "welcome_v1"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/campaigns/{campaign_id}/segments": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Upsert a segment whose DSL targets the tenant population",
        "description": "Creates (inserts) a segment under a campaign with an audience DSL document. The handler performs no body validation - an omitted dsl defaults to {} - so the only client-side edge cases are the path param and the campaign FK. Edge cases: an unknown or non-UUID campaign_id fails the FK/cast inside the insert and surfaces as an unhandled 500 rather than a 404; every call inserts a new segment row (there is no upsert on campaign_id), so this is not idempotent; population_estimate is left null until the compute endpoint is called.",
        "operationId": "post_api_campaigns_campaign_id_segments",
        "parameters": [
          {
            "name": "campaign_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "campaign_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "segment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "dsl": {
                      "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "match": "all",
                      "rules": [
                        {
                          "attr": "status",
                          "op": "eq",
                          "value": "active"
                        }
                      ]
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dsl": {
                    "type": "object",
                    "properties": {
                      "tenant_id": {
                        "type": "string"
                      },
                      "match": {
                        "type": "string"
                      },
                      "rules": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "attr": {
                              "type": "string"
                            },
                            "op": {
                              "type": "string"
                            },
                            "value": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "dsl": {
                  "tenant_id": "<id from auth>",
                  "match": "all",
                  "rules": [
                    {
                      "attr": "status",
                      "op": "eq",
                      "value": "active"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/campaigns": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Create a marketing campaign for the signed-up tenant",
        "description": "Creates a campaign for a tenant. tenant_id and name are both required; variant_flag_id is optional and links the campaign to a feature-flag variant for A/B allocation. Edge cases: missing or empty tenant_id/name -> 400; campaign names are not unique, so repeated POSTs create distinct campaigns (not idempotent); the new campaign starts in its default status and emits a campaign.created.v1 audit entry - audit failures are swallowed and do not fail the request.",
        "operationId": "post_api_campaigns",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "campaign_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "variant_flag_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "variant_flag_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "variant_flag_id": "<id from flags>"
              }
            }
          }
        }
      }
    },
    "/api/campaigns/journeys/{journey_id}/runs": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Start a journey run for a subject persona",
        "description": "Starts a journey run for one subject persona; the run begins at current_step 0 in state 'active'. subject_persona_id is required. Edge cases: missing subject_persona_id -> 400; an unknown journey_id fails the FK inside the insert and surfaces as an unhandled 500 rather than a 404; nothing prevents starting a second concurrent run for the same (journey, persona) pair, so the endpoint is not idempotent and callers must de-duplicate.",
        "operationId": "post_api_campaigns_journeys_journey_id_runs",
        "parameters": [
          {
            "name": "journey_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "journey_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing subject_persona_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "subject_persona_id": "<id from personas>"
              }
            }
          }
        }
      }
    },
    "/api/campaigns/runs/{run_id}/advance": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Advance a journey run by one step",
        "description": "Advances a journey run by one step, marking it completed once current_step walks past the journey's step count. Edge cases: an unknown run_id returns 404; a run whose state is already anything other than 'active' (e.g. completed) is returned unchanged with 200 rather than erroring, so repeated advances on a finished run are a safe no-op; if the parent journey row is missing the run is immediately completed; each call on an active run is a real state change, so the endpoint is not idempotent while the run is active.",
        "operationId": "post_api_campaigns_runs_run_id_advance",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "advance_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/campaigns/segments/{segment_id}/compute": {
      "post": {
        "tags": [
          "sdk-campaign"
        ],
        "summary": "Recompute a segment's population estimate",
        "description": "Recomputes a segment's population_estimate and stamps last_computed_at. Only simple tenant_id equality predicates in the DSL are evaluated today; anything else yields an estimate of 0. Edge cases: an unknown segment_id returns 404; if projection.subject_view is missing (e.g. a fresh test environment) the count silently falls back to 0 rather than erroring; the call is safely repeatable/idempotent - each run overwrites the previous estimate; the emitted campaign.segment.computed.v1 audit records tenant 'unknown' when the parent campaign row cannot be read.",
        "operationId": "post_api_campaigns_segments_segment_id_compute",
        "parameters": [
          {
            "name": "segment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "segment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "compute_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/commands/{command_id}/ack": {
      "post": {
        "tags": [
          "sdk-command"
        ],
        "summary": "Robot acks a dispatched command",
        "description": "Ingests a command acknowledgement/result FROM the robot edge agent. Unlike the other command routes this handler has no requireAuth preHandler: the bearer token is expected to be the per-robot scoped credential minted by POST /api/assets/:asset_id/credentials and is verified inside ackCommandWithCredential rather than as a JWT. Note that the api-gateway default-deny authGate still fronts this path, so in enforce mode the request must additionally satisfy the gate. ok is a mandatory boolean; code, message and data are optional detail. Edge cases: an empty, expired or revoked credential is a 401; a credential that is valid but not scoped to this command's asset is a 403; a command in a state other than dispatched (already acked, still pending, cancelled) is a 409, which also makes the call non-idempotent - a replayed ack conflicts; ok sent as the string \"true\" fails the strict boolean check with a 400.",
        "operationId": "post_api_commands_command_id_ack",
        "parameters": [
          {
            "name": "command_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "command_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ack_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ok": true,
                    "code": "DONE",
                    "message": "executed",
                    "data": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ok (boolean) is required — ok (boolean) is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "credential not scoped to this command — credential not scoped to this command"
          },
          "404": {
            "description": "command not found — command not found"
          },
          "409": {
            "description": "command not in dispatched state — command not in dispatched state"
          },
          "500": {
            "description": "<underlying error message> — Ack processing failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ok": {
                    "type": "boolean"
                  },
                  "code": {
                    "type": "string"
                  },
                  "message": {
                    "type": "string"
                  },
                  "data": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "ok": true,
                "code": "DONE",
                "message": "executed",
                "data": {}
              }
            }
          }
        }
      }
    },
    "/api/commands/{command_id}/decision": {
      "post": {
        "tags": [
          "sdk-command"
        ],
        "summary": "Approve a pending risky command",
        "description": "Approves or rejects a risky command that was gated into the pending state at issue time; the decision is audited with the approving subject and optional reason. Returns 200 with the updated command. approved is a mandatory boolean, so the string \"true\" is rejected. tenant_id and decided_by come solely from the JWT tenant_id and sub claims - there is no body override. Edge cases: a token missing either claim is a 400; an unknown command_id and a command that is no longer pending (already approved, rejected, dispatched, or belonging to another tenant) are collapsed into the same 409 \"command not found or not pending\" - there is no 404 path, and this also makes a replayed decision a 409; reason is optional.",
        "operationId": "post_api_commands_command_id_decision",
        "parameters": [
          {
            "name": "command_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "command_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "approved": true,
                    "reason": "operator confirmed safe",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant context required — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "command not found or not pending — command not found or not pending"
          },
          "500": {
            "description": "<underlying error message> — Decision processing failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "approved": {
                    "type": "boolean"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "approved": true,
                "reason": "operator confirmed safe"
              }
            }
          }
        }
      }
    },
    "/api/commands/{command_id}": {
      "get": {
        "tags": [
          "sdk-command"
        ],
        "summary": "Read command status",
        "description": "Looks up a single command's status and result by id, scoped to the caller's tenant from the JWT tenant_id claim. Returns 200 with the command record including its lifecycle state (pending, dispatched, acked, rejected, ...). Edge cases: a token with no tenant_id claim is a 400; a command_id that exists but belongs to a different tenant is indistinguishable from one that does not exist - both are 404, which is the intended tenant-isolation behaviour; a non-UUID command_id fails the Postgres UUID cast inside the try block and is reported as a generic 500 rather than a 400; requires a valid JWT.",
        "operationId": "get_api_commands_command_id",
        "parameters": [
          {
            "name": "command_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "command_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "command_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant context required — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "command not found — command not found"
          },
          "500": {
            "description": "<underlying error message> — Command lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/commands": {
      "post": {
        "tags": [
          "sdk-command"
        ],
        "summary": "Issue a low-risk move command",
        "description": "Issues a command to a robot asset or one of its components. The request is authorized through ReBAC plus policy inside sdk-command; commands whose risk_class is high enough land in a pending state awaiting approval via POST /api/commands/:command_id/decision instead of dispatching immediately. Returns 201 with the command record. tenant_id falls back to the JWT tenant_id claim; the issuer is always the JWT sub. Edge cases: a token with no tenant_id or no sub is a 400; target_asset_id and type are both mandatory; a ReBAC/policy denial is a 403 carrying the authorization error message; an unknown target_asset_id or target_component_id is not pre-checked and surfaces as a generic 500; params and risk_class are optional.",
        "operationId": "post_api_commands",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "command_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_asset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_component_id": "{{var:component_id}}",
                    "type": "move",
                    "params": {
                      "x": 1,
                      "y": 2
                    },
                    "risk_class": "high",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tenant_id required — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "<CommandAuthorizationError message> — Command authorization denied"
          },
          "500": {
            "description": "<underlying error message> — Command issuance failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "target_asset_id": {
                    "type": "string"
                  },
                  "target_component_id": {
                    "type": "string"
                  },
                  "type": {
                    "type": "string"
                  },
                  "params": {
                    "type": "object",
                    "properties": {
                      "x": {
                        "type": "integer"
                      },
                      "y": {
                        "type": "integer"
                      }
                    }
                  },
                  "risk_class": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "target_asset_id": "<id from assets>",
                "target_component_id": "{{var:component_id}}",
                "type": "move",
                "params": {
                  "x": 1,
                  "y": 2
                },
                "risk_class": "high"
              }
            }
          }
        }
      }
    },
    "/api/commands/stream/{asset_id}": {
      "get": {
        "tags": [
          "sdk-command"
        ],
        "summary": "Edge agent subscribes to its asset's command stream",
        "description": "Per-asset command delivery stream. This is a WebSocket-only route ({ websocket: true }): an edge agent for a robot subscribes here and receives dispatched commands in real time as JSON frames from the command broker. On connect the server immediately pushes a { kind: \"hello\", asset_id, emitted_at } frame, and the broker subscription is torn down by the socket close handler. The path prefix /api/commands/stream/ is deliberately exempt from the api-gateway default-deny authGate (WS auth belongs in the Sec-WebSocket-Protocol token, tracked as follow-up hardening), so no Authorization header is checked and any asset_id may be subscribed. The handler has no validation branches and never sends an HTTP error status: it does not verify the asset exists, so subscribing to an unknown asset_id simply yields a stream that never emits, and a send on an already-closed socket is swallowed. Edge cases are transport-level rather than status-code-level - a plain non-upgrade HTTP GET is rejected by the @fastify/websocket plugin before this handler runs, and an abandoned socket is cleaned up only on close.",
        "operationId": "get_api_commands_stream_asset_id",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "101": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "stream_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/connectors/dlq/replay": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Bulk-replay a tenant's dead-lettered syncs",
        "description": "Replay dead-lettered connector-sync items from the DLQ (connectors.sync_deadletter). With a deadletter_id it replays that single item (404 if unknown or already resolved); with tenant_id (and optional connector_kind) it bulk-replays every still-dead-lettered item for that tenant and returns replayed_count. Replayed items are marked 'retrying', their attempt count is bumped, and next_retry_at is set to now() so the retry/backoff worker re-drives them.",
        "operationId": "post_api_connectors_dlq_replay",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "connector_kind": "github",
                    "replay_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — provide deadletter_id or tenant_id"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — deadletter_id not found or already resolved"
          },
          "409": {
            "description": "ReplayFailed — replay failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "connector_kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "connector_kind": "github"
              }
            }
          }
        }
      }
    },
    "/api/connectors/dlq/retry-tick": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Run a retry tick (nothing due -> zero counts)",
        "description": "Drain one batch of due connector-sync dead-letters on demand: claims up to batch_size (default 20) entries in status dlq/retrying whose next_retry_at is past (FOR UPDATE SKIP LOCKED, concurrency-safe), re-drives each install's sync, and settles it — resolved on success, re-queued with exponential backoff on failure, or discarded once attempts reach max_attempts. The same logic runs on a timer when CONNECTORS_RETRY_WORKER_ENABLED. Returns per-tick counts; all zero when nothing is due.",
        "operationId": "post_api_connectors_dlq_retry_tick",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "retry_tick_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "batch_size": 20,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "batch_size": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "batch_size": 20
              }
            }
          }
        }
      }
    },
    "/api/connectors/inbound/{kind}": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Subscription-verification handshake echoes the challenge",
        "description": "Generic inbound webhook receiver for a connector kind (unauthenticated — signature-gated). Unknown kind -> 404. An unsigned subscription-verification handshake ({ challenge }) is echoed back with 200 (standard across providers). Real event deliveries must carry a valid x-connector-signature HMAC (HMAC-SHA256 over the body with CONNECTORS_INBOUND_SECRET) -> 202 accepted, else 401.",
        "operationId": "post_api_connectors_inbound_kind",
        "parameters": [
          {
            "name": "kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "kind path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "inbound_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "type": "url_verification",
                    "challenge": "verify-token-abc123",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "InvalidSignature — missing or invalid x-connector-signature"
          },
          "404": {
            "description": "UnknownConnectorKind — no connector kind '<kind>'"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string"
                  },
                  "challenge": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "type": "url_verification",
                "challenge": "verify-token-abc123"
              }
            }
          }
        }
      }
    },
    "/api/connectors": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List connector installs for a tenant (tenant-scoped query)",
        "description": "Tenant-scoped connector install summary served directly by the api-gateway (not sdk-connectors): selects vendor, install_id, status, last_synced_at, last_error and installed_at from connectors.install for the tenant_id query param, ordered by vendor. QA edge cases: the handler swallows ALL database errors and returns { success: true, data: [] } — that fallback exists because connectors.install is absent in some deploys, so an empty array can mean 'no installs', 'table missing', or 'query failed', and this endpoint can never return a 5xx from the query path; the only rejection is a missing tenant_id (400); tenant_id is cast to ::uuid, so a non-UUID value is caught by the same swallow and also returns an empty list rather than a 400; there is no limit or pagination, and no filter by status, so uninstalled/errored installs are included and must be filtered client-side.",
        "operationId": "get_api_connectors",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "connector_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/installs/{install_id}/health": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Health snapshot for a freshly installed connector",
        "description": "Health snapshot for a connector install: its status, whether an adapter is registered for its kind, tool count, open dead-lettered syncs, last sync cursor timestamp, and an overall healthy flag (active + adapter registered + no open DLQ). 404 if the install does not exist.",
        "operationId": "get_api_connectors_installs_install_id_health",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/installs/{install_id}": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Read a connector install by id",
        "description": "Fetches one connector install by install_id, returning its full record (tenant, kind, display name, status, vendor account, credential reference, sync metadata). QA edge cases: an unknown install_id returns a bare 404 { error: 'NotFound' } with no details array, and a malformed (non-UUID) install_id returns the same 404, so the two cases cannot be told apart by status; an uninstalled connector is still readable — uninstall flips status rather than deleting the row, so this returns 200 with the terminal status instead of 404; the lookup is by install_id alone with no tenant predicate, so an authenticated caller holding another tenant's install_id reads that record.",
        "operationId": "get_api_connectors_installs_install_id",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/installs/{install_id}/sync": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Sync a connector install (slack stub returns 0 records when unconfigured)",
        "description": "Triggers a resilient sync for one connector install. The status code encodes the outcome: 200 when the sync completed cleanly, and 202 when it failed transiently — in which case the item has been dead-lettered into connectors.sync_deadletter and will be re-driven by the retry/backoff worker. Only configuration-class errors escape as a 409. QA edge cases: the 200-vs-202 split is the thing to assert, because a 202 is NOT a success — it means the sync failed and was queued for retry, and a test that accepts any 2xx will pass against a completely broken connector; an unknown install_id is a 409 SyncFailed ('install <id> not found'), NOT a 404, and an install whose connector_kind has no registered adapter is also a 409 ('no adapter registered for <kind>'); syncing is not idempotent — each call performs real vendor work and can add another dead-letter row; concurrent syncs on one install are not guarded against.",
        "operationId": "post_api_connectors_installs_install_id_sync",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "sync_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "SyncFailed — install <install_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/connectors/installs/{install_id}/tools/call": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Invoke a connector tool by name (slack stub returns NotConfigured payload with 200)",
        "description": "Invokes one tool exposed by a connector install, passing args through to the adapter and returning its result. QA edge cases: only tool_name is validated at the edge (400 if absent) — args defaults to {} and is never schema-checked, so a call with wrong or missing arguments reaches the vendor and comes back as a 409 ToolCallFailed rather than a 400; every downstream failure collapses into that single 409, so assert on details to separate them — unknown install ('install <id> not found'), an install whose status is not active ('install <id> is <status>', i.e. an uninstalled or errored connector), a tool_name absent from the manifest ('tool <name> not in manifest for install <id>'), and a missing adapter ('no adapter registered for <kind>'); note the not-found and inactive-install cases are 409s, never 404 or 423; the call is not idempotent — it performs real vendor-side work, so retries can double-apply an effect.",
        "operationId": "post_api_connectors_installs_install_id_tools_call",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tool_name": "slack.message.post",
                    "args": {
                      "channel": "C0DEMO",
                      "text": "Hello from ProjexCloud"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing tool_name"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "ToolCallFailed — install <install_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tool_name": {
                    "type": "string"
                  },
                  "args": {
                    "type": "object",
                    "properties": {
                      "channel": {
                        "type": "string"
                      },
                      "text": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tool_name": "slack.message.post",
                "args": {
                  "channel": "C0DEMO",
                  "text": "Hello from ProjexCloud"
                }
              }
            }
          }
        }
      }
    },
    "/api/connectors/installs/{install_id}/tools": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List tool manifest for an install",
        "description": "Returns the tool manifests advertised by a connector install — the MCP-style tool definitions the agent runtime can invoke through POST /installs/:install_id/tools/call. QA edge cases: an unknown install_id returns 200 with an empty tools array rather than a 404, so this endpoint cannot be used to test install existence; an install whose adapter has never synced also returns an empty array, meaning \"unknown install\", \"no adapter\" and \"not yet synced\" are all indistinguishable; the returned tool_name values are exactly the strings /tools/call validates against, so a name absent from this manifest produces a 409 ToolCallFailed there; the listing is unpaginated and not tenant-scoped.",
        "operationId": "get_api_connectors_installs_install_id_tools",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tool_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/installs/{install_id}/uninstall": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Uninstall a connector install",
        "description": "Uninstalls a connector install, flipping its status and recording the acting persona. actor_id is read from the body and silently defaults to the literal string 'unknown' when omitted — it is never validated, so a bad or missing actor still succeeds and the audit trail records 'unknown', which is worth asserting. QA edge cases: an unknown install_id returns 404 NotFound; whether uninstalling an ALREADY-uninstalled install is idempotent (200 again) or returns 404 depends on whether the underlying UPDATE still matches the row, so cover the repeat call explicitly; the row is not deleted, so GET /installs/:install_id continues to return 200 afterwards with the terminal status — do not assert a 404 on the subsequent read.",
        "operationId": "post_api_connectors_installs_install_id_uninstall",
        "parameters": [
          {
            "name": "install_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "install_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "uninstall_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "actor_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/connectors/installs": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Install slack connector",
        "description": "Registers a connector install for a tenant and returns 201 with the install record. tenant_id, connector_kind and installed_by are required; display_name, credential_ref and vendor_account_id are optional. QA edge cases: connector_kind is NOT validated against the registered adapter list at install time, so an install can be created for a kind that has no adapter — the failure only appears later when /sync or /tools/call returns 409 'no adapter registered for <kind>', and that deferred failure is the key thing to cover; nothing enforces uniqueness on (tenant_id, connector_kind, vendor_account_id), so the call is not idempotent and repeated POSTs create duplicate installs for the same vendor account; credential_ref is stored as an opaque reference with no existence check against sdk-secrets, so a dangling ref installs cleanly and only fails at sync time; all three missing-field cases collapse into one generic 400 'missing fields' with no indication of which field was absent.",
        "operationId": "post_api_connectors_installs",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "connector_kind": "slack",
                    "display_name": "Slack Workspace",
                    "credential_ref": "secret://connectors/slack/demo",
                    "vendor_account_id": "T0DEMO",
                    "installed_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "connector_kind": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "credential_ref": {
                    "type": "string"
                  },
                  "vendor_account_id": {
                    "type": "string"
                  },
                  "installed_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "connector_kind": "slack",
                "display_name": "Slack Workspace",
                "credential_ref": "secret://connectors/slack/demo",
                "vendor_account_id": "T0DEMO",
                "installed_by": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/connectors/kinds": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List registered connector adapter kinds",
        "description": "Returns the list of connector kinds for which an adapter is currently registered in the sdk-connectors adapter registry, as { data: { kinds: [...] } }. QA edge cases: the list is built from the in-process registry, not from the database, so it reflects which adapter modules this gateway build actually loaded — a kind present in the catalogue but not compiled into the image will be absent, and that absence is exactly what makes POST /api/connectors/inbound/:kind return 404 UnknownConnectorKind for the same value; the response is global and identical for every tenant (no tenant scoping); it takes no parameters, has no pagination, and cannot return 404 or 409 — an empty kinds array is a valid 200 meaning no adapters registered.",
        "operationId": "get_api_connectors_kinds",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "kind_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/slack/events": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "URL verification handshake (no signature required)",
        "description": "Slack Events API webhook receiver. This route is deliberately on the api-gateway public allowlist and carries no requireAuth — a bearer JWT is neither required nor consulted, and the trust boundary is instead the Slack signature check over the raw request body using SLACK_SIGNING_SECRET together with the x-slack-request-timestamp and x-slack-signature headers. QA edge cases: the url_verification handshake responds 200 with the challenge as plain text (Content-Type: text/plain), NOT as JSON, so a test asserting a JSON body will fail on the very first call Slack makes; a bad, absent, or replayed-beyond-the-timestamp-window signature returns 401 InvalidSignature; every other outcome — including an event type this build does not handle — returns 200, because Slack retries any non-2xx and an error status would cause duplicate deliveries, so 'unrecognised event' must be asserted on the response body rather than the status; signature verification runs over rawBody, so any middleware that re-serialises the JSON breaks it; Slack redelivers on timeout, so handlers must tolerate duplicate event ids.",
        "operationId": "post_api_connectors_slack_events",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "type": "url_verification",
                    "challenge": "test-challenge-string",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "InvalidSignature — InvalidSignature"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string"
                  },
                  "challenge": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "type": "url_verification",
                "challenge": "test-challenge-string"
              }
            }
          }
        }
      }
    },
    "/api/connectors/slack/install": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "OAuth exchange returns 503 when SLACK_CLIENT_ID/SECRET not configured",
        "description": "Exchanges a Slack OAuth authorisation code for a workspace access token via oauth.v2.access, using SLACK_CLIENT_ID / SLACK_CLIENT_SECRET from the environment. On success the caller is expected to feed the returned team_id and access_token into POST /api/connectors/installs — this endpoint does NOT create the install itself, which is the integration gap worth testing. QA edge cases: a missing environment configuration is a 503 NotConfigured, distinct from the 400 you get for a missing code, so an unconfigured environment fails loudly rather than silently; Slack's own rejections (invalid_code, code_already_used, bad redirect_uri) come back as HTTP 400 SlackError with Slack's error string in details, NOT as a 401 — and because OAuth codes are single-use, replaying the same code returns invalid_code, so this call is explicitly non-idempotent; only a thrown transport error (network failure, malformed Slack response) becomes a 500 InternalError; redirect_uri is optional and passed through unvalidated.",
        "operationId": "post_api_connectors_slack_install",
        "parameters": [],
        "responses": {
          "400": {
            "description": "ValidationError — code required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          },
          "503": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "code": "test-oauth-code",
                    "redirect_uri": "https://example.com/slack/callback",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "redirect_uri": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "code": "test-oauth-code",
                "redirect_uri": "https://example.com/slack/callback"
              }
            }
          }
        }
      }
    },
    "/api/connectors/slack/post-message": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "postMessage returns 400 SlackError when SLACK_BOT_TOKEN not configured",
        "description": "Server-side wrapper over Slack chat.postMessage. channel and text are both required and must be non-empty strings; blocks (array) and thread_ts (string) are optional passthroughs. QA edge cases: Slack-side rejections are returned as HTTP 400 SlackError carrying Slack's error code in details — so channel_not_found, not_in_channel, invalid_auth and a revoked or missing bot token ALL arrive as 400, never as 401/403/404, and tests must assert details rather than status; a non-string or empty channel/text is a local 400 ValidationError before any Slack call; only a thrown transport error becomes a 500 InternalError; the call is not idempotent — there is no dedupe key, so a retry posts a second visible message into the channel, which makes at-least-once retry logic dangerous here; blocks is passed through only when it is an array, and a malformed blocks payload surfaces as a Slack 400 rather than local validation.",
        "operationId": "post_api_connectors_slack_post_message",
        "parameters": [],
        "responses": {
          "400": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "post_message_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "C0DEMO",
                    "text": "Hello from ProjexCloud",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "channel": "C0DEMO",
                "text": "Hello from ProjexCloud"
              }
            }
          }
        }
      }
    },
    "/api/connectors/tenants/{tenant_id}/dlq": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List a tenant's DLQ (empty is a valid result)",
        "description": "List dead-lettered connector-sync items for a tenant, newest failure first. Optional query filters: status (dlq/retrying/resolved/discarded), connector_kind, and limit. Returns an empty array when the tenant has no dead-lettered syncs.",
        "operationId": "get_api_connectors_tenants_tenant_id_dlq",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "dlq_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/tenants/{tenant_id}/dlq/reconcile": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Reconcile a tenant's DLQ (clean queue -> zero counts)",
        "description": "Reconcile duplicate/partial connector-sync DLQ state for a tenant. Collapses superseded duplicates (several active dlq/retrying entries sharing the same install_id + sync_kind + external_ref — only the newest is kept, older ones marked resolved) and requeues entries stuck in 'retrying' past the stale window (worker crashed mid-drive) back to 'dlq'. Idempotent: returns {superseded:0, requeued:0} on a clean queue.",
        "operationId": "post_api_connectors_tenants_tenant_id_dlq_reconcile",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reconcile_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/connectors/tenants/{tenant_id}/installs": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List connector installs for a tenant",
        "description": "Lists every connector install belonging to one tenant, keyed on the :tenant_id path param. QA edge cases: an unknown tenant_id returns 200 with an empty array rather than a 404, so 'no such tenant' and 'tenant with no installs' are indistinguishable; the tenant_id comes from the URL and is NOT cross-checked against the tenant claim in the caller's JWT, so an authenticated caller can enumerate another tenant's installs by changing the path segment — the cross-tenant isolation assertion belongs here; the listing is unpaginated with no limit/offset and no status filter, so uninstalled and errored installs are returned alongside active ones.",
        "operationId": "get_api_connectors_tenants_tenant_id_installs",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/consent/purposes": {
      "get": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "List consent purposes for the tenant",
        "description": "Returns the consent.purpose catalogue (purpose_id, app_id, description, legal_basis, default_jurisdictions, created_at) ordered by purpose_id and hard-capped at 200 rows. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: tenant_id is required as a query param and its absence is a 400, but consent.purpose is an app-scoped catalogue with no tenant_id column - the value is only presence-checked and never used in the SQL, so every caller sees the same global catalogue no matter which tenant_id (even a bogus or foreign one) they pass; there is no paging, so a catalogue larger than 200 rows is silently truncated; an empty catalogue is a 200 with an empty data array, not a 404.",
        "operationId": "get_api_consent_purposes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "purpos_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "QueryFailed — <postgres error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/consent/receipts": {
      "get": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "List consent receipts filtered by tenant, subject and purpose",
        "description": "Lists consent receipts where the tenant_id query param matches either source_tenant_id or target_tenant_id, optionally narrowed by subject_persona_id and purpose_id, newest-granted first and hard-capped at 200 rows. Status is derived rather than stored: revoked_at IS NULL renders as \"active\", otherwise \"revoked\". Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: tenant_id is required (400 when absent) and is taken from the query string rather than the JWT, so a caller may request another tenant receipts - verify tenant scoping deliberately; subject_persona_id is matched against the person_id column, and both it and tenant_id are cast to uuid, so a non-UUID value is a 500 rather than a 400; no matches is a 200 with an empty array; there is no paging, so results beyond 200 are silently dropped.",
        "operationId": "get_api_consent_receipts",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "receipt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "QueryFailed — <postgres error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/consent/receipts/{receipt_id}/revoke": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Revoke a previously granted consent receipt",
        "description": "Revokes a consent receipt by stamping consent.receipt.revoked_at = now() for the given receipt_id, returning {success:true}. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: the handler runs a bare UPDATE and never inspects the affected row count, so revoking a receipt_id that does not exist still returns 200 success:true - there is no 404; it is idempotent in effect but not in value, because a second call overwrites revoked_at with a later timestamp; the optional body reason is accepted but never persisted; there is no tenant check at all - the receipt is matched on receipt_id alone, so a caller can revoke another tenant receipt; a non-UUID receipt_id fails the comparison in Postgres and surfaces as a 500.",
        "operationId": "post_api_consent_receipts_receipt_id_revoke",
        "parameters": [
          {
            "name": "receipt_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "receipt_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "User requested withdrawal of consent.",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "QueryFailed — <postgres error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "User requested withdrawal of consent."
              }
            }
          }
        }
      }
    },
    "/api/consents/check": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Check the granted receipt is active",
        "description": "Returns the current consent state for a (person_id, purpose_id, processor, jurisdiction) tuple. This is the hot-path gate every downstream SDK calls before processing PII (FR-CNS-1, FR-CNS-5). Always returns 200 with the evaluated result - a tuple with no receipt, an expired receipt or a revoked receipt is reported as not-allowed in the payload rather than as a 404 or 403. All four fields are mandatory non-empty strings and are trimmed. Edge cases: whitespace-only values count as missing; an unknown person_id or purpose_id yields a negative result, not an error; the call is read-only, side-effect-free and safely repeatable; requires a valid JWT.",
        "operationId": "post_api_consents_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "purpose_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "processor": "tenant",
                    "jurisdiction": "US-CA",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "purpose_id": {
                    "type": "string"
                  },
                  "processor": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "purpose_id": "<id from consents>",
                "processor": "tenant",
                "jurisdiction": "US-CA"
              }
            }
          }
        }
      }
    },
    "/api/consents/export": {
      "get": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Export receipts for the freshly-registered person",
        "description": "Exports consent receipts as a JSONL-friendly array under { data: { receipts } } with status 200 - the DSAR / portability surface. The person_id query parameter is optional: when supplied the export is filtered to that person, and when omitted EVERY receipt in the store is returned. Edge cases: the export is unpaginated and has no limit, so calling it without person_id on a large store returns the entire table in one response; an unknown person_id returns 200 with an empty receipts array rather than a 404; revoked and expired receipts are included (this is a full history, not a live-consent view); no tenant scoping is applied - the only filter is person_id; requires a valid JWT.",
        "operationId": "get_api_consents_export",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "export_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/consents": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Grant consent for the registered purpose",
        "description": "Grants a new consent receipt for the (person, purpose, processor, jurisdiction) tuple (FR-CNS-1) and returns 201 with the receipt. person_id, purpose_id, processor, app_id, jurisdiction and granted_by_actor are all mandatory non-empty strings; expires_at is optional but must parse as ISO-8601 when supplied; source_tenant_id and target_tenant_id are optional and drive the cross-border check. Edge cases: a cross-border transfer refused on jurisdictional grounds returns HTTP 451 CrossBorderViolation (FR-CNS-5), not a 403; an existing active receipt for the same tuple is a 409, so granting twice is not idempotent - revoke first; a purpose_id that is not in the registry trips a foreign-key violation that is mapped to a 400 rather than a 404; an expires_at in the past is accepted (only parseability is validated); requires a valid JWT.",
        "operationId": "post_api_consents",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "consent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "purpose_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "processor": "tenant",
                    "app_id": "healthcare",
                    "jurisdiction": "US-CA",
                    "granted_by_actor": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "expires_at": "2026-01-15T10:30:00Z",
                    "source_tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — Active receipt already exists for this tuple"
          },
          "451": {
            "description": "CrossBorderViolation — <CrossBorderError message>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "purpose_id": {
                    "type": "string"
                  },
                  "processor": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  },
                  "granted_by_actor": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  },
                  "source_tenant_id": {
                    "type": "string"
                  },
                  "target_tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "purpose_id": "<id from consents>",
                "processor": "tenant",
                "app_id": "healthcare",
                "jurisdiction": "US-CA",
                "granted_by_actor": "<id from auth>",
                "expires_at": "<generated futuredatetime>",
                "source_tenant_id": "<id from auth>",
                "target_tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/consents/{receipt_id}/revoke": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Revoke the previously granted receipt",
        "description": "Revokes an existing consent receipt by appending a revocation row and stamping the parent receipt (FR-CNS-2). Returns 200 with the revocation record. Both revoked_by and reason are mandatory non-empty strings and are trimmed, so whitespace-only values count as missing. Edge cases: an unknown receipt_id is a 404; where the service reports an already-revoked or otherwise non-revocable receipt with a \"not found\" style error it also surfaces as a 404, so replaying a revoke does not silently succeed; a non-UUID receipt_id fails the Postgres UUID cast and, not matching the \"not found\" substring, is reported as a generic 500; requires a valid JWT.",
        "operationId": "post_api_consents_receipt_id_revoke",
        "parameters": [
          {
            "name": "receipt_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "receipt_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "revoked_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "User requested withdrawal of marketing consent.",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <service \"not found\" message>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "revoked_by": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "revoked_by": "<id from auth>",
                "reason": "User requested withdrawal of marketing consent."
              }
            }
          }
        }
      }
    },
    "/api/consents/purposes": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "Register a marketing-email purpose",
        "description": "Registers a typed processing purpose in the per-app purpose registry (FR-CNS-4) and emits consent.purpose.registered.v1. Returns 201 with the purpose. purpose_id, app_id and description are mandatory non-empty strings; legal_basis must be one of consent | contract | legitimate-interest | vital | public-task | legal-obligation; default_jurisdictions is optional, defaults to an empty array, and must be an array of strings when supplied. Edge cases: re-registering an existing purpose_id is a 409 rather than an upsert, so the call is not idempotent; string fields are trimmed, so whitespace-only values count as missing; a non-array or mixed-type default_jurisdictions is a 400; requires a valid JWT.",
        "operationId": "post_api_consents_purposes",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "purpos_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "purpose_id": "sample-slug",
                    "app_id": "healthcare",
                    "description": "Send transactional and marketing emails about appointments and offers.",
                    "legal_basis": "consent",
                    "default_jurisdictions": [
                      "US-CA",
                      "EU"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — purpose_id already registered"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "purpose_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "legal_basis": {
                    "type": "string"
                  },
                  "default_jurisdictions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "purpose_id": "<generated slug>",
                "app_id": "healthcare",
                "description": "Send transactional and marketing emails about appointments and offers.",
                "legal_basis": "consent",
                "default_jurisdictions": [
                  "US-CA",
                  "EU"
                ]
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "List the registry and see the purpose just registered",
        "description": "Reads the purpose registry that POST /api/consents/purposes writes into. Optional query filters app_id, category and legal_basis are ANDed; limit (default 100, clamped 1..500) and offset page the result, and total reports the unpaged count so a caller can distinguish a complete taxonomy from a truncated first page. Ordered by (app_id, purpose_id) for a stable page boundary. The registry is a single platform-wide namespace and this reports it as one: consent.purpose.purpose_id is a global TEXT primary key, so two tenants cannot both register the same id and the second already learns of the first through a 409 on register - scoping the read while leaving the write globally unique would hide names the write path reveals on the next collision. Read-only, side-effect-free and safely repeatable. Requires a valid JWT, or a key holding consent.purpose.read (note this is a DIFFERENT scope from consent.check.write).",
        "operationId": "get_api_consents_purposes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — limit and offset must be numbers"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Service token is missing required scope: consent.purpose.read"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/content/items/{item_id}/archive": {
      "post": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Archive a draft content item",
        "description": "Archives a content item (a soft state change; the row and its versions are retained and still readable via GET). Takes no body. Edge cases: an unknown :item_id returns 404; a non-UUID :item_id fails in Postgres as a 500; there is no already-archived guard, so repeating the call returns 200 with the same row (idempotent in effect, not by check); archiving does not unpublish a previously published version, and there is no un-archive counterpart on this route.",
        "operationId": "post_api_content_items_item_id_archive",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "item_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "archive_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/content/items/{item_id}": {
      "get": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Fetch a content item by id",
        "description": "Fetches a content item by id. Edge cases: an unknown :item_id returns 404 NotFound; the read is NOT tenant-filtered — the handler looks the item up by id alone, so any authenticated caller holding an item_id can read another tenant's item; a non-UUID :item_id is not validated and fails in Postgres as a 500; archived items are still returned (archive is a status, not a delete) and the response carries the item row only, not its versions.",
        "operationId": "get_api_content_items_item_id",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "item_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/content/items/{item_id}/versions": {
      "get": {
        "tags": [
          "sdk-content"
        ],
        "summary": "List versions for a content item",
        "description": "Lists the versions of a content item. Edge cases: this is a list endpoint, so an unknown :item_id returns 200 with data.versions = [] rather than 404 — a typo'd item is indistinguishable from an item with no versions yet; there is no paging or limit parameter, so an item with a long edit history returns every version in one response; results are not tenant-filtered; a non-UUID :item_id fails in Postgres as a 500.",
        "operationId": "get_api_content_items_item_id_versions",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "item_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Create a content version with body, media and tags",
        "description": "Appends a new draft version to an item. Every body field is optional — payload defaults to {}, media_refs and taxonomy_tags default to undefined — so an EMPTY body legitimately creates an empty version and there is no 400 branch on this route at all. Edge cases: an unknown or non-UUID :item_id is not checked by the handler and fails on the item_id foreign key / uuid cast as a 500 rather than 404; versions are append-only and each call creates a NEW version even with identical content (not idempotent); the created version is a draft and is not live until the publish route runs; media_refs and taxonomy_tags are stored as given and are not validated to exist.",
        "operationId": "post_api_content_items_item_id_versions",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "item_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "payload": {
                      "title": "Acme QA Sample",
                      "body": "Draft body copy for the first version."
                    },
                    "media_refs": [
                      "media://hero-image"
                    ],
                    "taxonomy_tags": [
                      "news",
                      "featured"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload": {
                    "type": "object",
                    "properties": {
                      "title": {
                        "type": "string"
                      },
                      "body": {
                        "type": "string"
                      }
                    }
                  },
                  "media_refs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "taxonomy_tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "payload": {
                  "title": "<generated name>",
                  "body": "Draft body copy for the first version."
                },
                "media_refs": [
                  "media://hero-image"
                ],
                "taxonomy_tags": [
                  "news",
                  "featured"
                ]
              }
            }
          }
        }
      }
    },
    "/api/content/items/{item_id}/versions/{version_id}/publish": {
      "post": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Publish a version on behalf of the signed-up user",
        "description": "Publishes a specific version of an item, making it the live version and recording published_by. Edge cases: published_by is the only required body field (400 if absent) and is accepted as free text — it is not verified to be a real persona; the 404 covers the (item_id, version_id) PAIR, so a valid version_id belonging to a DIFFERENT item is a 404 rather than a cross-item publish; there is no state guard, so re-publishing the already-live version returns 200 again (idempotent in effect); publishing an older version is permitted and acts as a rollback.",
        "operationId": "post_api_content_items_item_id_versions_version_id_publish",
        "parameters": [
          {
            "name": "item_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "item_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "published_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "publish_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing published_by"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "published_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "published_by": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/content/items": {
      "post": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Create a draft article content item",
        "description": "Creates a content item (the container that versions hang off): tenant_id, type_code and slug are required, owner_persona_id is optional. Edge cases: only presence is validated — tenant_id comes from the body rather than the JWT and is never checked against the caller's tenant, so cross-tenant creation is not blocked here; a tenant_id or owner_persona_id that is not a UUID, or references no row, fails downstream in Postgres as a 500 rather than a 400/404; slug uniqueness is enforced only by a DB constraint, so a duplicate (tenant, slug) surfaces as a 500 duplicate-key rather than a 409; the item is created with NO version — POST .../versions afterwards.",
        "operationId": "post_api_content_items",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "item_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "type_code": "article",
                    "slug": "Acme QA Sample",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "type_code": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "type_code": "article",
                "slug": "<generated name>",
                "owner_persona_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/content/taxonomies": {
      "put": {
        "tags": [
          "sdk-content"
        ],
        "summary": "Upsert a tenant taxonomy with a small structure tree",
        "description": "Upserts a tenant's named taxonomy tree. tenant_id and name are required; structure is optional and defaults to {} — so a PUT with no structure BLANKS the stored tree rather than leaving it untouched, the main destructive edge case here. Idempotent by (tenant_id, name): re-PUTting overwrites and returns 200, never 201, so it is safe to replay but cannot be used for partial updates. Edge cases: tenant_id comes from the body, not the JWT, and is not checked against the caller's tenant; a non-UUID or non-existent tenant_id fails in Postgres as a 500; structure is stored as opaque JSON and its shape is never validated.",
        "operationId": "put_api_content_taxonomies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "taxonomy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "structure": {
                      "root": "topics",
                      "children": [
                        {
                          "code": "news",
                          "label": "News"
                        },
                        {
                          "code": "guides",
                          "label": "Guides"
                        }
                      ]
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "structure": {
                    "type": "object",
                    "properties": {
                      "root": {
                        "type": "string"
                      },
                      "children": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "code": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "structure": {
                  "root": "topics",
                  "children": [
                    {
                      "code": "news",
                      "label": "News"
                    },
                    {
                      "code": "guides",
                      "label": "Guides"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/crm/activities/call": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Log an answered outbound call with recording consent",
        "description": "Log a call on the contact/lead timeline with its structured telephony fields: direction, disposition, duration, phone number, recording url + recording consent, and the provider call id. Emits crm.call.missed.v1 when the disposition means nobody picked up (no_answer/busy/failed) and crm.call.logged.v1 otherwise. Edge cases: writes are IDEMPOTENT on external_call_id (partial unique index) because telephony webhooks retry — re-logging the same provider call id UPDATES the existing timeline entry in place rather than appending a duplicate, and deliberately does NOT re-emit the domain event, so downstream consumers cannot double-count one call; validation happens before the insert so an unknown call_disposition, an unknown call_direction, or a negative call_duration_seconds returns a 400 listing every offending field instead of a raw Postgres CHECK violation surfacing as a 500; 400 when encounter_id or actor_persona_id is missing; activities with no external_call_id (manually logged calls) are unconstrained and may repeat.",
        "operationId": "post_api_crm_activities_call",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "call_direction": "outbound",
                    "call_disposition": "answered",
                    "call_duration_seconds": 137,
                    "phone_number": "+14155550123",
                    "recording_url": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                    "recording_consent": true,
                    "external_call_id": "CA{{dynamic:slug}}",
                    "summary": "Discovery call - budget and timeline confirmed",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — encounter_id and actor_persona_id are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "encounter_id": {
                    "type": "string"
                  },
                  "actor_persona_id": {
                    "type": "string"
                  },
                  "call_direction": {
                    "type": "string"
                  },
                  "call_disposition": {
                    "type": "string"
                  },
                  "call_duration_seconds": {
                    "type": "integer"
                  },
                  "phone_number": {
                    "type": "string"
                  },
                  "recording_url": {
                    "type": "string"
                  },
                  "recording_consent": {
                    "type": "boolean"
                  },
                  "external_call_id": {
                    "type": "string"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "encounter_id": "<id from encounters>",
                "actor_persona_id": "<id from personas>",
                "call_direction": "outbound",
                "call_disposition": "answered",
                "call_duration_seconds": 137,
                "phone_number": "+14155550123",
                "recording_url": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                "recording_consent": true,
                "external_call_id": "CA{{dynamic:slug}}",
                "summary": "Discovery call - budget and timeline confirmed",
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/crm/activities/calls": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Read the call timeline for the encounter",
        "description": "Read the call and voicemail entries on one encounter's timeline, newest first, optionally narrowed to a kind (call|voicemail) or a call_disposition. Only telephony activities are returned — note/email/meeting/task entries are excluded even though they share the crm.activity table. Edge cases: 400 when the encounter_id query param is missing; an unknown encounter yields an empty array rather than 404, since this is a filtered read not a record fetch; limit defaults to 50 and offset to 0; entries logged manually (no external_call_id) appear alongside webhook-sourced ones.",
        "operationId": "get_api_crm_activities_calls",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — encounter_id query param required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/activities": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Log an activity inside an encounter (encounter_id + kind + actor_persona_id required)",
        "description": "Logs a generic CRM activity on an encounter timeline. encounter_id, kind and actor_persona_id are required, and kind must be one of the ACTIVITY_KINDS enum defined in the crm model (widened with 'voicemail' by migration 003). Edge cases: missing required fields -> 400 'missing fields'; a kind outside the enum -> 400 'invalid activity kind'; summary and occurred_at are optional and occurred_at defaults server-side when omitted (a future-dated occurred_at is not rejected); there is no de-duplication key, so retrying the same log creates a second activity - unlike POST /api/crm/activities/call this endpoint is not idempotent; an unknown encounter_id fails the FK inside the insert and surfaces as an unhandled 500.",
        "operationId": "post_api_crm_activities",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "activity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "note",
                    "actor_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "summary": "Follow-up call with prospect",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "encounter_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "actor_persona_id": {
                    "type": "string"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "encounter_id": "<id from encounters>",
                "kind": "note",
                "actor_persona_id": "<id from personas>",
                "summary": "Follow-up call with prospect",
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/crm/activities/voicemail": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Log an outbound voicemail with transcript",
        "description": "Log a voicemail on the contact/lead timeline, optionally with its transcript, as activity kind 'voicemail' (added by migration 003). call_disposition defaults to 'voicemail' (the call reached the contact's voicemail) and may be 'left_message' when the rep recorded one — any OTHER disposition is rejected, since a voicemail activity cannot be 'answered' or 'busy'. Emits crm.voicemail.received.v1, which is deliberately distinct from crm.call.missed.v1 so reaching voicemail is not double-counted as a plain missed call in follow-up reporting. Edge cases: idempotent on external_call_id exactly like the call endpoint (retry updates in place and does not re-emit the event); 400 when encounter_id or actor_persona_id is missing, when the disposition is outside voicemail|left_message, when call_direction is not inbound|outbound, or when call_duration_seconds is negative.",
        "operationId": "post_api_crm_activities_voicemail",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "voicemail_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "call_direction": "outbound",
                    "call_disposition": "left_message",
                    "call_duration_seconds": 18,
                    "phone_number": "+14155550123",
                    "recording_url": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                    "recording_consent": true,
                    "voicemail_transcript": "Hi, following up on your demo request - call me back when you get a chance.",
                    "external_call_id": "CA{{dynamic:slug}}",
                    "summary": "Left voicemail after no answer",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — encounter_id and actor_persona_id are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "encounter_id": {
                    "type": "string"
                  },
                  "actor_persona_id": {
                    "type": "string"
                  },
                  "call_direction": {
                    "type": "string"
                  },
                  "call_disposition": {
                    "type": "string"
                  },
                  "call_duration_seconds": {
                    "type": "integer"
                  },
                  "phone_number": {
                    "type": "string"
                  },
                  "recording_url": {
                    "type": "string"
                  },
                  "recording_consent": {
                    "type": "boolean"
                  },
                  "voicemail_transcript": {
                    "type": "string"
                  },
                  "external_call_id": {
                    "type": "string"
                  },
                  "summary": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "encounter_id": "<id from encounters>",
                "actor_persona_id": "<id from personas>",
                "call_direction": "outbound",
                "call_disposition": "left_message",
                "call_duration_seconds": 18,
                "phone_number": "+14155550123",
                "recording_url": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                "recording_consent": true,
                "voicemail_transcript": "Hi, following up on your demo request - call me back when you get a chance.",
                "external_call_id": "CA{{dynamic:slug}}",
                "summary": "Left voicemail after no answer",
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/crm/contacts/{contact_id}": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Fetch an existing contact by id",
        "description": "Fetches a single CRM contact by contact_id. Edge cases: an unknown contact_id returns 404; the route takes no tenant_id param and does not re-check tenant ownership beyond the JWT gate, so QA should confirm cross-tenant reads are blocked upstream; a malformed (non-UUID) contact_id fails the Postgres uuid cast and surfaces as an unhandled 500, not a 404.",
        "operationId": "get_api_crm_contacts_contact_id",
        "parameters": [
          {
            "name": "contact_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "contact_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Update contact lifecycle_stage on an existing contact",
        "description": "Partially updates a CRM contact; the whole request body is passed through as the field patch. Edge cases: an unknown contact_id returns 404; an empty body is accepted and results in a no-op update; field names are not whitelisted at the route, so unknown keys are handled by the service layer; the patch is idempotent - applying the same body twice yields the same row; a malformed (non-UUID) contact_id surfaces as an unhandled 500 from the uuid cast.",
        "operationId": "patch_api_crm_contacts_contact_id",
        "parameters": [
          {
            "name": "contact_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "contact_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "lifecycle_stage": "customer",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "custom_fields": {
                      "tier": "gold"
                    },
                    "external_refs": {
                      "hubspot_id": "HS-002"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lifecycle_stage": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  },
                  "custom_fields": {
                    "type": "object",
                    "properties": {
                      "tier": {
                        "type": "string"
                      }
                    }
                  },
                  "external_refs": {
                    "type": "object",
                    "properties": {
                      "hubspot_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "lifecycle_stage": "customer",
                "owner_persona_id": "<id from personas>",
                "custom_fields": {
                  "tier": "gold"
                },
                "external_refs": {
                  "hubspot_id": "HS-002"
                }
              }
            }
          }
        }
      }
    },
    "/api/crm/contacts": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Create persona-keyed contact (tenant_id + persona_id required)",
        "description": "Creates a CRM contact binding a persona to a tenant, with optional lifecycle_stage, source, owner_persona_id, custom_fields and external_refs. tenant_id and persona_id are both required. Edge cases: missing or empty tenant_id/persona_id -> 400; lifecycle_stage is not enum-validated at this route; there is no uniqueness guard on (tenant_id, persona_id), so posting twice creates two contacts - the endpoint is not idempotent; a persona_id that does not exist fails the FK inside the insert and surfaces as an unhandled 500 rather than a 400/404.",
        "operationId": "post_api_crm_contacts",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "lifecycle_stage": "lead",
                    "source": "web",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "custom_fields": {
                      "segment": "enterprise"
                    },
                    "external_refs": {
                      "salesforce_id": "SF-001"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "lifecycle_stage": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  },
                  "custom_fields": {
                    "type": "object",
                    "properties": {
                      "segment": {
                        "type": "string"
                      }
                    }
                  },
                  "external_refs": {
                    "type": "object",
                    "properties": {
                      "salesforce_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "lifecycle_stage": "lead",
                "source": "web",
                "owner_persona_id": "<id from personas>",
                "custom_fields": {
                  "segment": "enterprise"
                },
                "external_refs": {
                  "salesforce_id": "SF-001"
                }
              }
            }
          }
        }
      }
    },
    "/api/crm/deals/{deal_id}": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Get the created deal",
        "description": "Fetch a single deal with enriched pipeline fields (funnel_stage_id, priority, fit, forecast, stage-aging anchors). Tenant-scoped via tenant_id query param. 404 if not found for the tenant.",
        "operationId": "get_api_crm_deals_deal_id",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "deal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Enrich the deal (priority/fit/forecast)",
        "description": "Update a deal's richer pipeline fields (priority, fit, pain/impact/outcome, decision_date, offer_version, forecast, stakeholders, amount, currency, funnel_stage_id, stage). Changing stage or funnel_stage_id re-stamps the stage-aging anchors (entered_stage_at / last_stage_change_at). tenant_id is required. 404 if not found.",
        "operationId": "patch_api_crm_deals_deal_id",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "deal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "priority": "high",
                    "fit": "strong",
                    "pain": "Manual data entry",
                    "impact": "20h/week saved",
                    "outcome": "Automated pipeline",
                    "forecast": "commit",
                    "close_probability": 70,
                    "stakeholders": [
                      "economic-buyer",
                      "champion"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "priority": {
                    "type": "string"
                  },
                  "fit": {
                    "type": "string"
                  },
                  "pain": {
                    "type": "string"
                  },
                  "impact": {
                    "type": "string"
                  },
                  "outcome": {
                    "type": "string"
                  },
                  "forecast": {
                    "type": "string"
                  },
                  "close_probability": {
                    "type": "integer"
                  },
                  "stakeholders": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "priority": "high",
                "fit": "strong",
                "pain": "Manual data entry",
                "impact": "20h/week saved",
                "outcome": "Automated pipeline",
                "forecast": "commit",
                "close_probability": 70,
                "stakeholders": [
                  "economic-buyer",
                  "champion"
                ]
              }
            }
          }
        }
      }
    },
    "/api/crm/deals/{deal_id}/transition": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Transition an existing deal to a valid stage",
        "description": "Moves a deal to a new stage through the stage guard, which enforces transition validity, stage entry criteria, and terminal-stage gating; the stage-changed event is emitted only when the move is permitted. Valid stages are qualifying, proposal, negotiation, closed-won and closed-lost. Edge cases: an absent or non-listed stage -> 400; an unknown deal_id -> 404 (from either the null result or a DealNotFoundError); a disallowed move - e.g. out of a terminal closed-won/closed-lost stage, a skipped stage, or unmet stage criteria - is 409 InvalidTransition with the guard's reason in details; re-transitioning a deal to the stage it is already in is evaluated by the same guard rather than short-circuiting.",
        "operationId": "post_api_crm_deals_deal_id_transition",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "stage": "proposal",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid stage"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — <StageTransitionError message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "stage": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "stage": "proposal"
              }
            }
          }
        }
      }
    },
    "/api/crm/deals/{deal_id}/next-action/complete": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Complete the next action",
        "description": "Complete the deal's open NEXT action with an outcome (status -> completed). 404 if there is no open action. After completing, the deal needs a new NEXT action to pass the save-gate again (unless terminal). tenant_id required.",
        "operationId": "post_api_crm_deals_deal_id_next_action_complete",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "complete_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "outcome": "Spoke with buyer; budget confirmed",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — no open NEXT action to complete"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "outcome": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "outcome": "Spoke with buyer; budget confirmed"
              }
            }
          }
        }
      }
    },
    "/api/crm/deals/{deal_id}/next-action": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Get the open next action",
        "description": "Get the deal's current open NEXT action. 404 if none is open. tenant_id query param required.",
        "operationId": "get_api_crm_deals_deal_id_next_action",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "next_action_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — no open NEXT action for this deal"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Set a follow-up call as the next action",
        "description": "Set (replace) the deal's single open NEXT action: type, owner, due-time and purpose. Any prior open action is cancelled so there is always exactly one open. tenant_id and due_at are required; 404 if the deal does not exist.",
        "operationId": "post_api_crm_deals_deal_id_next_action",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "next_action_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "action_type": "call",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "due_at": "2026-01-15T10:30:00Z",
                    "purpose": "Confirm budget and timeline",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and due_at are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — deal not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "action_type": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  },
                  "due_at": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "action_type": "call",
                "owner_persona_id": "<id from personas>",
                "due_at": "<generated futuredatetime>",
                "purpose": "Confirm budget and timeline"
              }
            }
          }
        }
      }
    },
    "/api/crm/deals/{deal_id}/save-gate": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Save-gate allows (open next action present)",
        "description": "The save-gate verdict: a non-terminal deal may save/advance only if it has an open NEXT action; terminal deals (closed-won/lost or an is_terminal funnel stage) are always allowed. Returns {allowed, reason, is_terminal, has_open_next_action}. tenant_id query param required.",
        "operationId": "get_api_crm_deals_deal_id_save_gate",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "save_gate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — deal not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/deals/{deal_id}/stage-guard": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Guard allows qualifying -> proposal",
        "description": "Evaluate whether a deal may transition to to_stage: validates against the injectable allowed-transition map, runs the entry/exit criteria hook, and enforces terminal gating (a terminal stage closed-won/closed-lost has no permitted exits; a same-stage move is a no-op). Returns {allowed, reason, from_stage, to_stage, is_terminal_exit}. tenant_id and to_stage query params required.",
        "operationId": "get_api_crm_deals_deal_id_stage_guard",
        "parameters": [
          {
            "name": "deal_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "deal_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "stage_guard_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — deal not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/deals": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "List deals for a tenant",
        "description": "List a tenant's deals, optionally filtered by stage, newest first. Paginated via limit/offset query params.",
        "operationId": "get_api_crm_deals",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "deal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Create deal on an encounter (tenant_id + encounter_id + name required)",
        "description": "Creates a deal on an encounter. tenant_id, encounter_id and name are required; contact_id, amount, currency, close_probability, custom_fields and external_refs are optional. Edge cases: any of the three required fields missing or empty -> 400; amount and close_probability are not range-checked here (negative or >100 values pass the route); a non-existent encounter_id or contact_id fails the FK inside the insert and surfaces as an unhandled 500; deal names are not unique, so repeat POSTs create duplicates - the endpoint is not idempotent.",
        "operationId": "post_api_crm_deals",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "deal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "amount": 5000,
                    "currency": "USD",
                    "close_probability": 50,
                    "custom_fields": {
                      "priority": "high"
                    },
                    "external_refs": {
                      "crm_ref": "DEAL-001"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "encounter_id": {
                    "type": "string"
                  },
                  "contact_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "close_probability": {
                    "type": "integer"
                  },
                  "custom_fields": {
                    "type": "object",
                    "properties": {
                      "priority": {
                        "type": "string"
                      }
                    }
                  },
                  "external_refs": {
                    "type": "object",
                    "properties": {
                      "crm_ref": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "encounter_id": "<id from encounters>",
                "contact_id": "<id from crm>",
                "name": "<generated name>",
                "amount": 5000,
                "currency": "USD",
                "close_probability": 50,
                "custom_fields": {
                  "priority": "high"
                },
                "external_refs": {
                  "crm_ref": "DEAL-001"
                }
              }
            }
          }
        }
      }
    },
    "/api/crm/funnel-stages": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "List funnel stages",
        "description": "List a tenant's configurable pipeline stages in board order (sort_order).",
        "operationId": "get_api_crm_funnel_stages",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "funnel_stage_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Create a Discovery stage",
        "description": "Create a per-tenant configurable pipeline stage (name, sort_order, criteria, default win-probability, is_default/is_terminal/is_won). UNIQUE per (tenant, name). tenant_id and name are required.",
        "operationId": "post_api_crm_funnel_stages",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "funnel_stage_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "sort_order": 1,
                    "description": "Initial qualification",
                    "criteria": "Budget confirmed",
                    "probability": 20,
                    "is_default": false,
                    "is_terminal": false,
                    "is_won": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and name are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "sort_order": {
                    "type": "integer"
                  },
                  "description": {
                    "type": "string"
                  },
                  "criteria": {
                    "type": "string"
                  },
                  "probability": {
                    "type": "integer"
                  },
                  "is_default": {
                    "type": "boolean"
                  },
                  "is_terminal": {
                    "type": "boolean"
                  },
                  "is_won": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "sort_order": 1,
                "description": "Initial qualification",
                "criteria": "Budget confirmed",
                "probability": 20,
                "is_default": false,
                "is_terminal": false,
                "is_won": false
              }
            }
          }
        }
      }
    },
    "/api/crm/pipeline/board": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Get the pipeline board",
        "description": "Pipeline kanban board: open deals grouped by stage with per-stage count + total_amount, ordered by recent activity. Tenant-scoped; empty array when the tenant has no open deals.",
        "operationId": "get_api_crm_pipeline_board",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "board_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/pipeline/stale": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "List stale deals",
        "description": "Open deals whose current stage has aged past business_days (default 5, weekends excluded) since last_stage_change_at. Business-day aware, not calendar-day. Tenant-scoped.",
        "operationId": "get_api_crm_pipeline_stale",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "stale_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/data-rights/executions/{execution_id}/result": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Record success",
        "description": "Records the terminal outcome of a single DSAR execution: status is required and is expected to be 'succeeded' or 'failed', with an optional audit_entry_id sealing the proof and an optional error_detail on failure. Edge cases: a missing status -> 400 (note the value itself is not enum-checked at the route, so any non-empty string is accepted); an unknown execution_id -> 404; posting a result for an execution that already has one overwrites the previous outcome rather than 409-ing, so retries are idempotent for a given body.",
        "operationId": "post_api_data_rights_executions_execution_id_result",
        "parameters": [
          {
            "name": "execution_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "execution_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "result_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "succeeded",
                    "audit_entry_id": "{{var:audit_entry_id}}",
                    "error_detail": null,
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing status"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  },
                  "audit_entry_id": {
                    "type": "string"
                  },
                  "error_detail": {
                    "nullable": true
                  }
                }
              },
              "example": {
                "status": "succeeded",
                "audit_entry_id": "{{var:audit_entry_id}}",
                "error_detail": null
              }
            }
          }
        }
      }
    },
    "/api/data-rights/reconciliation/run": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Run reconciliation (empty discrepancies -> green)",
        "description": "Records a reconciliation run comparing expected vs actual data classes per (person, pool) and flips the traffic light that gates DSAR certificate issuance. Edge cases: the body is entirely optional - an omitted discrepancies array defaults to [], which records a clean (green) run and unblocks certificate issuance, so QA must treat an empty-body call as semantically meaningful rather than a no-op; discrepancy entries are not shape-validated at the route; the endpoint always answers 200 (not 201) and every call appends a new run, making the most recent run authoritative.",
        "operationId": "post_api_data_rights_reconciliation_run",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "discrepancies": [],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "discrepancies": {
                    "type": "array",
                    "items": {}
                  }
                }
              },
              "example": {
                "discrepancies": []
              }
            }
          }
        }
      }
    },
    "/api/data-rights/requests": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Submit erasure request",
        "description": "Submits a DSAR (Data Subject Access Request) and creates a new data_rights.request row in submitted status. Requires person_id and kind; optional tenant_id, jurisdiction, and approval_policy. kind must be one of the six DSAR kinds (access, erasure, rectification, restriction, objection, portability) — any other value is rejected 400. Returns 201 with the created request; edge cases: missing person_id, missing kind, invalid/unknown kind.",
        "operationId": "post_api_data_rights_requests",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "erasure",
                    "jurisdiction": "GDPR",
                    "approval_policy": "manager-approval",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  },
                  "approval_policy": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from scim>",
                "tenant_id": "<id from auth>",
                "kind": "erasure",
                "jurisdiction": "GDPR",
                "approval_policy": "manager-approval"
              }
            }
          }
        }
      }
    },
    "/api/data-rights/requests/{request_id}/certificate": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Issue cert with shred proofs",
        "description": "Issues the completion certificate for a DSAR request, sealing it with an audit entry whose entry_id becomes signed_by_audit_entry_id (a caller-supplied signed_by_audit_entry_id wins so an externally-orchestrated flow can pre-seal). Guarded by the reconciliation traffic light: if the latest reconciliation run is red the request is refused 409 before anything is written. Edge cases: the whole body is optional - shred_proofs defaults to {} and an empty proof set is accepted; an unknown request_id is NOT rejected here, the certificate is still inserted with the request_id as its subject fallback; the endpoint is not idempotent - each call inserts another certificate row for the same request.",
        "operationId": "post_api_data_rights_requests_request_id_certificate",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "certificate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "shred_proofs": {
                      "admin-us-east-1": "audit-hash-base64"
                    },
                    "artifact_s3_key": "s3://dsar-certs/erasure-cert.pdf",
                    "signed_by_audit_entry_id": "{{var:signed_by_audit_entry_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "ReconciliationRed — Reconciliation red — DSAR completion blocked"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "shred_proofs": {
                    "type": "object",
                    "properties": {
                      "admin-us-east-1": {
                        "type": "string"
                      }
                    }
                  },
                  "artifact_s3_key": {
                    "type": "string"
                  },
                  "signed_by_audit_entry_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "shred_proofs": {
                  "admin-us-east-1": "audit-hash-base64"
                },
                "artifact_s3_key": "s3://dsar-certs/erasure-cert.pdf",
                "signed_by_audit_entry_id": "{{var:signed_by_audit_entry_id}}"
              }
            }
          }
        }
      }
    },
    "/api/data-rights/requests/{request_id}": {
      "get": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Get DSAR request",
        "description": "Fetches one DSAR request with its current lifecycle status, kind, jurisdiction and SLA deadline. Edge cases: an unknown request_id returns 404; the route takes no tenant param, so tenant isolation rests on the JWT gate and QA should verify a foreign request_id is not readable; a malformed (non-UUID) request_id fails the uuid cast and surfaces as an unhandled 500 rather than a 404.",
        "operationId": "get_api_data_rights_requests_request_id",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/data-rights/requests/{request_id}/plan-executions": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Plan executions",
        "description": "Fans a DSAR request out into one pending execution row per pool the subject has residency in, choosing the action from the request kind: erasure -> shred-person-key, access/portability -> export, everything else -> rectify. Edge cases: an unknown request_id is NOT a 404 - planExecutions returns [] and the route still answers 201 with an empty executions array; likewise a subject with no residency rows yields an empty plan, which is the signal to touch residency first; the endpoint is not idempotent - calling it twice inserts a second full set of execution rows for the same request.",
        "operationId": "post_api_data_rights_requests_request_id_plan_executions",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "plan_execution_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/data-rights/requests/{request_id}/transition": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Transition submitted -> identity-verified",
        "description": "Advances a DSAR request to a new lifecycle state. body.to must be one of submitted, identity-verified, approval-pending, grace-period, executing, certificate-issued, audited, rejected; approval_ref and grace_until are optional companions (grace_until is parsed as a Date). Edge cases: a missing or non-enum target state -> 400; an unknown request_id -> 404; a transition the state machine forbids from the request's current status -> 409 InvalidTransition carrying 'Invalid transition <current> -> <to>'; an unparseable grace_until becomes an Invalid Date rather than a 400; re-sending the same transition after it has been applied is itself an invalid transition and returns 409, so the endpoint is not idempotent.",
        "operationId": "post_api_data_rights_requests_request_id_transition",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to": "identity-verified",
                    "approval_ref": "{{var:approval_ref}}",
                    "grace_until": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid target state"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — Invalid transition <current status> → <to>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string"
                  },
                  "approval_ref": {
                    "type": "string"
                  },
                  "grace_until": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "to": "identity-verified",
                "approval_ref": "{{var:approval_ref}}",
                "grace_until": "<generated futuredatetime>"
              }
            }
          }
        }
      }
    },
    "/api/data-rights/residency/{person_id}": {
      "get": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "List residency",
        "description": "Lists all person_pool_residency rows for the given person_id path param. No body validation and no not-found branch — an unknown person_id returns 200 with an empty residency array (not 404). The only error surface is authentication.",
        "operationId": "get_api_data_rights_residency_person_id",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "residency_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/data-rights/residency/touch": {
      "post": {
        "tags": [
          "sdk-data-rights"
        ],
        "summary": "Record residency for a person+pool",
        "description": "Upserts (touches) a person_pool_residency record for a person+pool, returning 200 with the record. Requires person_id, pool_index and tenant_id; data_classes is optional and defaults to an empty array. Edge cases: each of the three required fields missing.",
        "operationId": "post_api_data_rights_residency_touch",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "touch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "pool_index": "admin-us-east-1",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "data_classes": [
                      "profile",
                      "persona"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "pool_index": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "data_classes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "person_id": "<id from scim>",
                "pool_index": "admin-us-east-1",
                "tenant_id": "<id from auth>",
                "data_classes": [
                  "profile",
                  "persona"
                ]
              }
            }
          }
        }
      }
    },
    "/api/deliverability/bounce-events": {
      "get": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "List bounce events",
        "description": "List a tenant's processed provider bounce/complaint events (newest first), optionally filtered by classification. tenant_id query param required.",
        "operationId": "get_api_deliverability_bounce_events",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "bounce_event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/deliverability/check": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Check a fresh address (not suppressed)",
        "description": "Pre-send enforcement: check whether a recipient (single address or batch addresses[]) is suppressed for the tenant or globally, on the given channel, before delivering. Returns {suppressed} for a single address or {results:[{address,suppressed}]} for a batch. Global suppression and channel=\"all\" both match.",
        "operationId": "post_api_deliverability_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "address": "qa.user@example.com",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and channel are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "address": "<generated email>"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/mailboxes": {
      "get": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "List mailboxes",
        "description": "List a tenant's IMAP mailboxes, newest first. tenant_id query param required.",
        "operationId": "get_api_deliverability_mailboxes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "mailbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Register an inbox",
        "description": "Register a tenant mailbox for inbound reply sync (upsert per tenant+username+folder). The reply worker polls it incrementally over IMAP by UID. tenant_id, imap_host and username are required; the IMAP secret is a vault ref (never raw).",
        "operationId": "post_api_deliverability_mailboxes",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "mailbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "imap_host": "imap.example.com",
                    "imap_port": 993,
                    "username": "replies-{{dynamic:uuid}}@example.com",
                    "secret_ref": "vault://imap/{{dynamic:uuid}}",
                    "folder": "INBOX",
                    "use_tls": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, imap_host and username are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "imap_host": {
                    "type": "string"
                  },
                  "imap_port": {
                    "type": "integer"
                  },
                  "username": {
                    "type": "string"
                  },
                  "secret_ref": {
                    "type": "string"
                  },
                  "folder": {
                    "type": "string"
                  },
                  "use_tls": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "imap_host": "imap.example.com",
                "imap_port": 993,
                "username": "replies-{{dynamic:uuid}}@example.com",
                "secret_ref": "vault://imap/{{dynamic:uuid}}",
                "folder": "INBOX",
                "use_tls": true
              }
            }
          }
        }
      }
    },
    "/api/deliverability/mailboxes/{mailbox_id}/replies": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Ingest a human reply",
        "description": "Ingest a single inbound reply (what the IMAP worker calls per fetched message; also the direct-capture surface). Classifies human vs auto_reply/OOO (RFC 3834 heuristics), records the reply_event (idempotent per mailbox+message_id), and fires pause-on-reply for a human reply. tenant_id and message_id required.",
        "operationId": "post_api_deliverability_mailboxes_mailbox_id_replies",
        "parameters": [
          {
            "name": "mailbox_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "mailbox_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reply_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "message_id": "reply-{{dynamic:uuid}}@example.com",
                    "from_address": "qa.user@example.com",
                    "subject": "Re: your proposal",
                    "snippet": "Thanks, looks good — lets talk.",
                    "in_reply_to": "sent-{{dynamic:uuid}}@projexcloud",
                    "references": "sent-abc@projexcloud",
                    "headers": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and message_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string"
                  },
                  "from_address": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "snippet": {
                    "type": "string"
                  },
                  "in_reply_to": {
                    "type": "string"
                  },
                  "references": {
                    "type": "string"
                  },
                  "headers": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "message_id": "reply-{{dynamic:uuid}}@example.com",
                "from_address": "<generated email>",
                "subject": "Re: your proposal",
                "snippet": "Thanks, looks good — lets talk.",
                "in_reply_to": "sent-{{dynamic:uuid}}@projexcloud",
                "references": "sent-abc@projexcloud",
                "headers": {}
              }
            }
          }
        }
      }
    },
    "/api/deliverability/mailboxes/{mailbox_id}/sync": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Sync the mailbox",
        "description": "Poll a mailbox once over IMAP: fetch messages after last_uid via the pluggable fetcher, capture each as a reply_event, and advance the UID cursor. Idempotent (already-captured messages skipped). Returns {fetched, captured, paused}. With no live IMAP client wired it returns zero. tenant_id required.",
        "operationId": "post_api_deliverability_mailboxes_mailbox_id_sync",
        "parameters": [
          {
            "name": "mailbox_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "mailbox_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "sync_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — mailbox not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/optout/redeem": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Redeem the opt-out token",
        "description": "Redeem an opt-out token (one-time): verify by hash, mark it used, add the suppression, and write an opt-out audit event. Returns 410 if the token is unknown, already used, or expired. token required.",
        "operationId": "post_api_deliverability_optout_redeem",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "redeem_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "token": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "feedback": "Too many emails",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — token is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "410": {
            "description": "Gone — token is unknown, already used, or expired"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "feedback": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "token": "<id from deliverability>",
                "feedback": "Too many emails"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/optout-tokens": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Issue an unsubscribe token",
        "description": "Mint a single-purpose opt-out (unsubscribe) token for an address. The raw token is returned ONCE (only its hash is persisted) — embed it in the unsubscribe link. tenant_id, channel and address required.",
        "operationId": "post_api_deliverability_optout_tokens",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "optout_token_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "address": "qa.user@example.com",
                    "purpose": "unsubscribe",
                    "scope": "tenant",
                    "ttl_seconds": 604800,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, channel and address are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  },
                  "ttl_seconds": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "address": "<generated email>",
                "purpose": "unsubscribe",
                "scope": "tenant",
                "ttl_seconds": 604800
              }
            }
          }
        }
      }
    },
    "/api/deliverability/reply-events": {
      "get": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "List reply events",
        "description": "List a tenant's captured inbound reply events (newest first), optionally filtered by classification. tenant_id query param required.",
        "operationId": "get_api_deliverability_reply_events",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "reply_event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/deliverability/reputation": {
      "get": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Get email reputation",
        "description": "Expose a tenant's send reputation signals to callers: all channels, or a single channel via ?channel=. Includes sent/bounce/complaint counts, derived rates and the sending status (good/watch/paused). tenant_id query param required.",
        "operationId": "get_api_deliverability_reputation",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "reputation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/deliverability/reputation/record": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Record a healthy batch (2% bounce -> good)",
        "description": "Increment per-(tenant,channel) send/delivery/bounce/complaint counters and recompute the reputation status. Auto-pauses the channel (status=paused) when the bounce rate >= 5% or complaint rate >= 0.1% over a minimum volume; a paused channel never auto-un-pauses. Returns the fresh reputation row. tenant_id required.",
        "operationId": "post_api_deliverability_reputation_record",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "record_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "sent": 100,
                    "delivered": 96,
                    "bounced": 2,
                    "complained": 0,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "sent": {
                    "type": "integer"
                  },
                  "delivered": {
                    "type": "integer"
                  },
                  "bounced": {
                    "type": "integer"
                  },
                  "complained": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "sent": 100,
                "delivered": 96,
                "bounced": 2,
                "complained": 0
              }
            }
          }
        }
      }
    },
    "/api/deliverability/reputation/resume": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Resume the email channel",
        "description": "Manually resume a channel that auto-paused for reputation (human override): clears the pause, resets the counter window, and sets status back to good. Returns the reset reputation row; 404 if no reputation row exists for the tenant/channel. tenant_id required.",
        "operationId": "post_api_deliverability_reputation_resume",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "resume_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — no reputation row for that tenant/channel"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/suppressions": {
      "get": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "List suppressions",
        "description": "List a tenant's suppressions (plus global rows), newest first, optionally filtered by channel. tenant_id query param required.",
        "operationId": "get_api_deliverability_suppressions",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "suppression_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Suppress an email (manual)",
        "description": "Add (or refresh) a suppression for an address on a channel. Idempotent per (scope-bucket, channel, address) — a repeat upserts the reason/detail. Addresses are sha256-hashed server-side (never stored raw). tenant_id, channel and address required.",
        "operationId": "post_api_deliverability_suppressions",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "suppression_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "address": "qa.user@example.com",
                    "reason": "manual",
                    "reason_detail": "Requested by customer",
                    "source": "support",
                    "scope": "tenant",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, channel and address are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "reason_detail": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "address": "<generated email>",
                "reason": "manual",
                "reason_detail": "Requested by customer",
                "source": "support",
                "scope": "tenant"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/suppressions/remove": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Remove a suppression",
        "description": "Remove (un-suppress) an address for a channel and scope. Idempotent — removing a non-existent suppression still returns 200. tenant_id, channel and address required.",
        "operationId": "post_api_deliverability_suppressions_remove",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "remove_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "address": "qa.user@example.com",
                    "scope": "tenant",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, channel and address are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "address": "<generated email>",
                "scope": "tenant"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/webhook-secrets": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Register a SendGrid signing secret",
        "description": "Register (or rotate) a tenant's HMAC signing secret for a provider's bounce/complaint webhook. Once set, inbound webhooks for that (tenant, provider) are signature-enforced. tenant_id, provider and signing_secret are required.",
        "operationId": "post_api_deliverability_webhook_secrets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "webhook_secret_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "provider": "sendgrid",
                    "signing_secret": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "algo": "sha256",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, provider and signing_secret are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "provider": {
                    "type": "string"
                  },
                  "signing_secret": {
                    "type": "string"
                  },
                  "algo": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "provider": "sendgrid",
                "signing_secret": "<generated uuid>",
                "algo": "sha256"
              }
            }
          }
        }
      }
    },
    "/api/deliverability/webhooks/{provider}": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Deliver a hard-bounce notification (no secret configured -> accepted, auto-suppress)",
        "description": "PUBLIC provider webhook receiver (on the gateway allowlist) — HMAC-verified when a signing secret is configured for the (tenant, provider), else accepted (dev/unconfigured). Classifies each event as hard_bounce/soft_bounce/complaint (SES/SendGrid/Mailgun/Postmark shapes + a normalized envelope) and AUTO-SUPPRESSES the recipient on hard bounce or complaint. tenant_id is carried on the per-tenant webhook URL via ?tenant_id=. Returns 401 if a configured signature fails to verify.",
        "operationId": "post_api_deliverability_webhooks_provider",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "webhook_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "event_type": "hard_bounce",
                    "address": "qa.user@example.com",
                    "message_id": "msg-{{dynamic:uuid}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "InvalidSignature — webhook HMAC signature verification failed"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "message_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "event_type": "hard_bounce",
                "address": "<generated email>",
                "message_id": "msg-{{dynamic:uuid}}"
              }
            }
          }
        }
      }
    },
    "/api/devices": {
      "post": {
        "tags": [
          "sdk-device"
        ],
        "summary": "Register iOS device",
        "description": "Registers a device, returning 201 with the device record. The insert is an upsert on device_uuid: re-registering an existing device updates os_version, app_version and last_seen_at and COALESCEs device_key_ref (an omitted key ref preserves the stored one) rather than conflicting - so the call is idempotent and never returns a 409. device_uuid and platform are mandatory; platform is validated against the closed set ios | android | web | desktop. Edge cases: an unrecognised platform is a 400; os_version, app_version and device_key_ref are optional and default to null; re-registering a device that was previously revoked does NOT reset its status back to active; requires a valid JWT.",
        "operationId": "post_api_devices",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "device_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "platform": "ios",
                    "os_version": "17.2",
                    "app_version": "1.0.0",
                    "device_key_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "os_version": {
                    "type": "string"
                  },
                  "app_version": {
                    "type": "string"
                  },
                  "device_key_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "<generated uuid>",
                "platform": "ios",
                "os_version": "17.2",
                "app_version": "1.0.0",
                "device_key_ref": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/devices/{device_uuid}/attest": {
      "post": {
        "tags": [
          "sdk-device"
        ],
        "summary": "Attest via secure-enclave",
        "description": "Records a device attestation, returning 201 with the attestation record. method is validated against the closed set secure-enclave | key-attestation | safetynet | play-integrity, and signature_envelope is a base64 string decoded into bytes and stored verbatim. Edge cases: an unrecognised method is a 400; the handler does not pre-check that the device exists, so attesting an unregistered device_uuid trips the attestation foreign key and escapes as an unhandled 500; expires_at is optional, defaults to null, and a past expiry is accepted without validation; verified is optional and defaults to false, so a bare attestation is recorded as unverified; every call appends a new attestation row, so the operation is not idempotent - repeat attestations accumulate; requires a valid JWT.",
        "operationId": "post_api_devices_device_uuid_attest",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "attest_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "method": "secure-enclave",
                    "signature_envelope": "YmFzZTY0ZW52ZWxvcGU=",
                    "expires_at": "2026-01-15T10:30:00Z",
                    "verified": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — insert or update on table \"attestation\" violates foreign key constraint"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "method": {
                    "type": "string"
                  },
                  "signature_envelope": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  },
                  "verified": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "method": "secure-enclave",
                "signature_envelope": "YmFzZTY0ZW52ZWxvcGU=",
                "expires_at": "<generated futuredatetime>",
                "verified": true
              }
            }
          }
        }
      }
    },
    "/api/devices/{device_uuid}": {
      "get": {
        "tags": [
          "sdk-device"
        ],
        "summary": "Get device",
        "description": "Reads one device record by device_uuid - platform, versions, key ref, status and the first/last seen timestamps - returning 200 with { data: { device } }. Read-only. Devices are platform-scoped rather than tenant-scoped, so the handler performs no tenant check and any authenticated caller can read any device_uuid. Edge cases: an unregistered device_uuid is a 404; a revoked device is still returned with status 200 (its status field reflects revoked/stolen) rather than 404ing; a non-UUID device_uuid reaches the Postgres UUID cast unguarded and surfaces as a 500; requires a valid JWT.",
        "operationId": "get_api_devices_device_uuid",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "device_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/devices/{device_uuid}/link-person": {
      "post": {
        "tags": [
          "sdk-device"
        ],
        "summary": "Link person to device",
        "description": "Links a person to a device (device_uuid path param) via linkPerson, returning 200 with the created link record. Requires person_id in the body; no not-found guard on device_uuid (an unknown device passes through to the service layer). Edge cases: missing person_id, auth failures.",
        "operationId": "post_api_devices_device_uuid_link_person",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "link_person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing person_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from scim>"
              }
            }
          }
        }
      }
    },
    "/api/devices/{device_uuid}/persons": {
      "get": {
        "tags": [
          "sdk-device"
        ],
        "summary": "List persons for device",
        "description": "Lists the person links for one device - every person that has been associated with this device_uuid, with each link's status and first/last used timestamps - returning 200 with { data: { links } }. Read-only and unpaginated: a device with many linked persons returns every row. Edge cases: an unregistered device_uuid, or one with no links yet, returns 200 with an empty links array rather than a 404; links whose status is no longer active are included in the listing; devices are platform-scoped so no tenant filter is applied; a non-UUID device_uuid reaches the Postgres UUID cast unguarded and surfaces as a 500; requires a valid JWT.",
        "operationId": "get_api_devices_device_uuid_persons",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/devices/{device_uuid}/revoke": {
      "post": {
        "tags": [
          "sdk-device"
        ],
        "summary": "Revoke device",
        "description": "Revokes a device by setting its status to the supplied reason and bumping last_seen_at, returning 200 with the updated device record. The body is optional: reason may be revoked or stolen and defaults to revoked when omitted. Edge cases: an unregistered device_uuid updates no rows and is reported as a 404; revoking an already-revoked device succeeds again and re-emits the device.revoked.v1 audit event, so the call is idempotent in effect but not silent; a reason outside revoked | stolen is NOT validated here and is written straight to the status column, where an invalid enum value surfaces as an unhandled 500; a non-UUID device_uuid fails the Postgres UUID cast and also escapes as a 500; requires a valid JWT.",
        "operationId": "post_api_devices_device_uuid_revoke",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "revoked",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — invalid input value for enum / invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "revoked"
              }
            }
          }
        }
      }
    },
    "/api/diagnostic/crash": {
      "get": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "List crashes for a registered device",
        "description": "Lists the crash snapshots recorded for one device, selected by the required device_uuid query param. Edge cases: a missing device_uuid query param -> 400; a device_uuid with no crashes is NOT a 404 - it returns 200 with an empty data array; the endpoint accepts no pagination or time-window params, so a chatty device returns its full crash history in one response.",
        "operationId": "get_api_diagnostic_crash",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "crash_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — device_uuid query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "Record a crash snapshot for a registered device",
        "description": "Intake for a mobile/desktop crash snapshot. device_uuid, app_version, os_version, stack_envelope and occurred_at are all required; person_id and tenant_id are optional and default to null for anonymous/pre-login crashes. Edge cases: any missing required field -> 400 listing all five; a stack_envelope that is oversized or fails the service-layer checks is also 400 (the service throw is caught and downgraded, so a storage failure looks like a validation failure to the client); occurred_at is not format-validated at the route, so a bad timestamp fails inside recordCrash and returns 400; there is no de-duplication key, so retried uploads create duplicate crash rows.",
        "operationId": "post_api_diagnostic_crash",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "crash_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_version": "1.2.3",
                    "os_version": "17.0",
                    "stack_envelope": "eyJzdGFjayI6IFtdfQ==",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — device_uuid, app_version, os_version, stack_envelope, occurred_at are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "person_id": {
                    "type": "string"
                  },
                  "app_version": {
                    "type": "string"
                  },
                  "os_version": {
                    "type": "string"
                  },
                  "stack_envelope": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "<id from devices>",
                "person_id": "<id from scim>",
                "app_version": "1.2.3",
                "os_version": "17.0",
                "stack_envelope": "eyJzdGFjayI6IFtdfQ==",
                "occurred_at": "<generated pastdatetime>",
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/diagnostic/crash/{id}": {
      "get": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "Fetch a crash by id",
        "description": "Fetches a single crash snapshot by its id. Edge cases: an unknown id returns 404 with {success:false,error:'not found'}; a malformed (non-UUID) id fails the Postgres uuid cast and surfaces as an unhandled 500 rather than a 404; the route applies no device or tenant filter, so any authenticated caller who knows an id can read that crash - worth confirming against tenant-isolation expectations.",
        "operationId": "get_api_diagnostic_crash_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "crash_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/diagnostic/health": {
      "get": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "Fetch the latest health snapshot for a registered device",
        "description": "Returns the most recent health snapshot for one device, selected by the required device_uuid query param. NOTE: the path ends in /health, so the gateway auth gate treats it as public - no bearer token is required and there is no 401. Edge cases: a missing device_uuid query param -> 400; a known device that has never posted a snapshot returns 404 'no snapshots recorded' (distinct from the 400 for an absent param); only the latest snapshot is returned - there is no history or pagination on this route.",
        "operationId": "get_api_diagnostic_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — device_uuid query param required"
          },
          "404": {
            "description": "NotFound — no snapshots recorded"
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "Record a health snapshot for a registered device",
        "description": "Intake for a periodic device health probe: device_uuid and captured_at are required, while permissions, battery_pct, wifi_state and sensor_state are optional and default to null/undefined. NOTE: the path ends in /health, so the gateway auth gate treats it as public - no bearer token is required and there is no 401. Edge cases: missing device_uuid or captured_at -> 400; a malformed captured_at is not caught at the route and fails inside recordHealth, which is downgraded to a 400 rather than a 500; battery_pct is not range-checked, so out-of-range values are stored as given; snapshots are append-only, so repeated posts from the same device are all retained.",
        "operationId": "post_api_diagnostic_health",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "permissions": {
                      "camera": true,
                      "location": false
                    },
                    "battery_pct": 85,
                    "wifi_state": "connected",
                    "sensor_state": {
                      "accelerometer": "ok"
                    },
                    "captured_at": "2026-01-15T10:30:00Z",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — device_uuid and captured_at are required"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "permissions": {
                    "type": "object",
                    "properties": {
                      "camera": {
                        "type": "boolean"
                      },
                      "location": {
                        "type": "boolean"
                      }
                    }
                  },
                  "battery_pct": {
                    "type": "integer"
                  },
                  "wifi_state": {
                    "type": "string"
                  },
                  "sensor_state": {
                    "type": "object",
                    "properties": {
                      "accelerometer": {
                        "type": "string"
                      }
                    }
                  },
                  "captured_at": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "<id from devices>",
                "permissions": {
                  "camera": true,
                  "location": false
                },
                "battery_pct": 85,
                "wifi_state": "connected",
                "sensor_state": {
                  "accelerometer": "ok"
                },
                "captured_at": "<generated pastdatetime>",
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/diagnostic/session-replay": {
      "post": {
        "tags": [
          "sdk-diagnostic-telemetry"
        ],
        "summary": "Record a sanitized session-replay event for a registered device",
        "description": "Intake for one sanitized session-replay event. device_uuid, sanitized_event_kind and occurred_at are required; payload is optional and is expected to be pre-sanitized by the client - the route performs no PII scrubbing of its own. Edge cases: any missing required field -> 400; sanitized_event_kind is not enum-validated at the route, so unknown kinds are rejected only if the service layer rejects them (which is downgraded to a 400); an oversized payload or a malformed occurred_at also surfaces as 400 rather than 500; events are append-only with no de-duplication, so replayed uploads duplicate rows.",
        "operationId": "post_api_diagnostic_session_replay",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "session_replay_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "sanitized_event_kind": "tap",
                    "payload": {
                      "x": 120,
                      "y": 340
                    },
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — device_uuid, sanitized_event_kind, occurred_at are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "sanitized_event_kind": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "x": {
                        "type": "integer"
                      },
                      "y": {
                        "type": "integer"
                      }
                    }
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "<id from devices>",
                "sanitized_event_kind": "tap",
                "payload": {
                  "x": 120,
                  "y": 340
                },
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/dispatch/routes/optimize": {
      "post": {
        "tags": [
          "sdk-dispatch"
        ],
        "summary": "Optimize a dispatcher route from queued tasks",
        "description": "P7 FR-DSP-3 — optimizes a dispatcher's stop order for a set of tasks: loads each task's lat/lng, runs nearest-neighbour plus a 2-opt improvement pass, estimates drive time from DISPATCH_AVG_SPEED_KMH (default 35), persists a dispatch.route row and returns {route_id, persona_id, stops[], optimized_at, total_drive_mins} at 200. QA edge cases: validation is a single branch — persona_id must be present AND task_ids must be a non-empty array, so {} , a missing persona_id, task_ids:[] and task_ids as a string all return the same 400. Beyond that there is no 404 path: task_ids that do not exist, or exist but have null lat/lng, are silently dropped by loadStops, and if that leaves zero stops the optimizer throws and the handler returns 500 — so a bad-FK test surfaces as 500, not 404. A single valid stop short-circuits the algorithm and returns a one-stop route with total_drive_mins 0. start_task_id is optional and, when it does not match any loaded stop, falls back to the first stop rather than erroring. persona_id is cast to ::uuid on INSERT, so a non-UUID persona_id also produces a 500. Not idempotent — every call mints a fresh route_id (dsr_<hex>) and inserts a new row; there is no dedupe on (persona_id, task_ids) and no cap on how many task_ids may be sent. No pagination. persona_id comes from the body and is not checked against the caller's JWT tenant, but the path is not on the gateway public allowlist so a valid tenant JWT is required.",
        "operationId": "post_api_dispatch_routes_optimize",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "optimize_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "task_ids": [
                      "{{var:dispatch_task_id}}"
                    ],
                    "start_task_id": "{{var:dispatch_task_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — persona_id and task_ids[] are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "RouteOptimizeFailed — [route-optimizer] no stops have lat/lng — refusing to optimize an empty route"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persona_id": {
                    "type": "string"
                  },
                  "task_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "start_task_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "persona_id": "<id from personas>",
                "task_ids": [
                  "{{var:dispatch_task_id}}"
                ],
                "start_task_id": "{{var:dispatch_task_id}}"
              }
            }
          }
        }
      }
    },
    "/api/dispatch/ws/{persona_id}": {
      "get": {
        "tags": [
          "sdk-dispatch"
        ],
        "summary": "Subscribe to live dispatch events for a persona",
        "description": "P7 FR-DSP-2 — WebSocket live-updates channel. On upgrade the connection subscribes to the in-process dispatch broker for :persona_id (server-side filtering, so the socket only receives that dispatcher's events) and is immediately sent a {kind:'hello', persona_id, emitted_at} frame; subsequent dispatch events stream as JSON frames until close, when the subscription is torn down. QA edge cases: this is a WebSocket-only route (@fastify/websocket `websocket: true` with no wsHandler), so a plain HTTP GET without the Upgrade/Connection/Sec-WebSocket-Key handshake headers gets 404 with an EMPTY body — an HTTP-only test harness cannot assert 200 here and must either perform a real upgrade or expect 404. :persona_id is accepted verbatim from the path with NO validation, NO existence check and NO tenant scoping: an unknown, non-UUID or empty-ish persona_id still upgrades successfully and simply yields a channel that never emits, so there is no 400 or 404 for a bad id. /api/dispatch/ws/ is on the authGate.ts WS bypass list (WS auth belongs in Sec-WebSocket-Protocol, tracked as follow-up hardening), so NO bearer token is required and no 401 is produced — matching requiresAuth:false. Subscribing twice for the same persona opens two independent sockets that each receive every event (no dedupe), and broker delivery is best-effort: a send on a socket closing mid-flight is swallowed rather than surfaced as an error.",
        "operationId": "get_api_dispatch_ws_persona_id",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ws_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "404": {
            "description": "NotFound — (empty response body - reply.code(404).send() is called with no payload)"
          }
        },
        "security": []
      }
    },
    "/api/empi/candidate-links/{link_id}/adjudicate": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Steward approves a candidate link (triggers reversible merge)",
        "description": "Records a steward verdict (approve merges the pair reversibly; reject marks the link rejected and stamps decided_at, feeding review-latency). Needs step_id from steward-review, and the acting persona MUST equal that step's approver or sdk-approval rejects it. 404 when the link is not the caller tenant's.",
        "operationId": "post_api_empi_candidate_links_link_id_adjudicate",
        "parameters": [
          {
            "name": "link_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "link_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "adjudicate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "step_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approve",
                    "reason": "Records confirmed as the same patient",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — step_id and decision (approve|reject) are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — empi: candidate link <link_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "step_id": {
                    "type": "string"
                  },
                  "decision": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "step_id": "<id from empi>",
                "decision": "approve",
                "reason": "Records confirmed as the same patient"
              }
            }
          }
        }
      }
    },
    "/api/empi/candidate-links": {
      "get": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "List candidate links in the high-confidence band",
        "description": "Steward review queue: POSSIBLY_SAME candidate links by confidence band (band=high|medium|low, or explicit min/max) and status. TENANT-SCOPED — results are restricted to the tenant on the calling credential; there is no tenant_id parameter and pre-2026-08 rows with no tenant are excluded.",
        "operationId": "get_api_empi_candidate_links",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "candidate_link_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <error message from queryCandidateLinksByBand>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/empi/merges": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Merge two person records (event-sourced, reversible)",
        "description": "Merges two persons as a reversible, event-sourced merge event; never destructive. Attributed to the calling tenant. NOTE: the merge acts on the global L1 identity.person, so its effect is visible to every tenant sharing that person — attribution is per tenant, the effect is not.",
        "operationId": "post_api_empi_merges",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "merge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "surviving_person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "merged_person_id": "{{var:person_id}}",
                    "link_id": "{{var:link_id}}",
                    "reason": "Duplicate patient records confirmed by steward",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — surviving_person_id, merged_person_id are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — empi: merge failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "surviving_person_id": {
                    "type": "string"
                  },
                  "merged_person_id": {
                    "type": "string"
                  },
                  "link_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "surviving_person_id": "<id from auth>",
                "merged_person_id": "{{var:person_id}}",
                "link_id": "{{var:link_id}}",
                "reason": "Duplicate patient records confirmed by steward"
              }
            }
          }
        }
      }
    },
    "/api/empi/metrics": {
      "get": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Read EMPI observability metrics",
        "description": "EMPI observability for the CALLING TENANT only: unresolved links, merge reversals, confidence distribution, calibration ECE + drift alert, per-band adjudicated outcomes (band_outcomes / high_risk_precision) and review latency (review_latency: median_minutes, p90_minutes over window_days). Null precision/median means nothing adjudicated yet — render 'not measured', not zero.",
        "operationId": "get_api_empi_metrics",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "metric_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <error message from getEmpiMetrics>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/empi/candidate-links/{link_id}/steward-review": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Queue a candidate link for steward review",
        "description": "Queues a candidate link for steward review via sdk-approval; returns pending step ids for adjudicate. Requires route_id (an approval route whose step approver is the persona the adjudicating caller will present). tenant_id in the body is IGNORED — the tenant comes from the credential.",
        "operationId": "post_api_empi_candidate_links_link_id_steward_review",
        "parameters": [
          {
            "name": "link_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "link_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "steward_review_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — route_id, tenant_id are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — empi: candidate link <link_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "route_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "route_id": "<id from approvals>"
              }
            }
          }
        }
      }
    },
    "/api/empi/merges/{merge_id}/unmerge": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Reverse a prior merge via compensating event",
        "description": "Reverses a merge with a compensating unmerge event and reopens the originating candidate link (clearing decided_at). 404 MergeNotFound when the id is unknown OR belongs to another tenant — deliberately indistinguishable.",
        "operationId": "post_api_empi_merges_merge_id_unmerge",
        "parameters": [
          {
            "name": "merge_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "merge_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "unmerge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "Merge reversed - records belong to distinct persons",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — empi: merge <merge_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "Merge reversed - records belong to distinct persons"
              }
            }
          }
        }
      }
    },
    "/api/encounters/{encounter_id}": {
      "get": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Fetch an encounter by id",
        "description": "Fetches one encounter by encounter_id, returning the full row: state, vault_key_ref, opened_at/closed_at/sealed_at, retention_policy, retention_expires_at, and the parent/address/billing references. QA edge cases: an unknown encounter_id returns a bare 404 { error: 'NotFound' } with no details array, and a malformed (non-UUID) id returns the same 404 shape, so status alone does not separate them; the query is NOT tenant-scoped — it selects purely on encounter_id, so an authenticated caller from tenant A who learns an id belonging to tenant B reads that row, which is the cross-tenant assertion worth writing here; sealed encounters remain readable (sealing shreds the Vault key, it does not delete the metadata row), so a sealed encounter returns 200 with sealed_at set and a vault_key_ref that no longer resolves to usable key material.",
        "operationId": "get_api_encounters_encounter_id",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/encounters/{encounter_id}/grants/check": {
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Check whether a grantee may call a method",
        "description": "Boolean authorisation probe used by downstream SDKs to gate non-participant access: returns { data: { allowed: true|false } } for a (encounter_id, grantee_persona_id, method) triple. It selects the single most recent non-revoked, non-expired grant for that grantee on that encounter and reports whether method appears in scope.methods, with '*' acting as a wildcard. QA edge cases: this endpoint returns 200 with allowed:false for every negative outcome — no grant, revoked grant, expired grant, wrong method, or an entirely nonexistent encounter all look identical, so never assert 403/404 for a denial; the ORDER BY issued_at DESC LIMIT 1 is the trap — when a grantee holds several concurrent grants, only the newest is consulted, so issuing a narrow grant after a broad one silently REVOKES the broader access at check time; a grant whose scope has no methods array evaluates to allowed:false; method matching is exact and case-sensitive apart from the '*' wildcard; the check is read-only and idempotent.",
        "operationId": "post_api_encounters_encounter_id_grants_check",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "grantee_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "method": "chart.read",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grantee_persona_id": {
                    "type": "string"
                  },
                  "method": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "grantee_persona_id": "<id from personas>",
                "method": "chart.read"
              }
            }
          }
        }
      }
    },
    "/api/encounters/{encounter_id}/grants": {
      "get": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "List active grants for an encounter",
        "description": "Lists the currently ACTIVE grants on an encounter, ordered by issued_at. The query filters to revoked_at IS NULL AND expires_at > now(), so revoked and expired grants are excluded server-side. QA edge cases: expiry is evaluated at query time, which makes this the natural place to test TTL behaviour — issue a grant with a short ttl_ms, confirm it appears, then confirm it disappears once expires_at passes, with no state change or explicit sweep required; a revoked grant vanishes from this list immediately, so the listing cannot be used to audit historical grants; an unknown encounter_id returns 200 with an empty array rather than a 404; the listing is unpaginated and keyed only on encounter_id with no tenant predicate.",
        "operationId": "get_api_encounters_encounter_id_grants",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "grant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Issue a scope-bounded encounter grant",
        "description": "Issues an Encounter Grant (FR-EN-5): a time- and scope-bounded authorisation letting a non-participant persona invoke listed methods on the encounter until expires_at, computed as now() + ttl_ms. Returns 201 with the grant and emits engagement.encounter.grant.issued.v1 (regulated retention). QA edge cases: all four of grantee_persona_id, issuer_persona_id, scope and ttl_ms are required, and the ttl_ms check is falsy-based — ttl_ms: 0 is therefore rejected as a missing field rather than treated as an immediate expiry, which is the boundary case to cover; a negative ttl_ms passes validation and produces a grant whose expires_at is already in the past, so it never appears in the grants listing and never authorises anything; nothing validates the shape of scope, so a scope without a methods array is accepted and yields a grant that authorises nothing at check time; issuing is not idempotent and there is no cap — repeated POSTs stack multiple concurrent grants for the same grantee, and the check endpoint uses only the most recently issued one; the encounter's existence and state are not verified, so an unknown encounter_id trips the foreign key as an uncaught 500 and granting on a sealed encounter succeeds.",
        "operationId": "post_api_encounters_encounter_id_grants",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "grant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "grantee_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "issuer_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "scope": {
                      "methods": [
                        "chart.read"
                      ]
                    },
                    "ttl_ms": 28800000,
                    "capability_token_ref": "cap-token-ref-001",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — <database error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grantee_persona_id": {
                    "type": "string"
                  },
                  "issuer_persona_id": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "object",
                    "properties": {
                      "methods": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "ttl_ms": {
                    "type": "integer"
                  },
                  "capability_token_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "grantee_persona_id": "<id from personas>",
                "issuer_persona_id": "<id from personas>",
                "scope": {
                  "methods": [
                    "chart.read"
                  ]
                },
                "ttl_ms": 28800000,
                "capability_token_ref": "cap-token-ref-001"
              }
            }
          }
        }
      }
    },
    "/api/encounters/{encounter_id}/participants": {
      "get": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "List participants for an encounter",
        "description": "Lists every participant ever attached to an encounter, ordered by joined_at, including their role, required flag, joined_at and left_at. QA edge cases: this returns ALL participants, not just currently-present ones — rows for people who have left are included with a non-null left_at, so a caller wanting the active roster must filter client-side; an unknown encounter_id returns 200 with an empty array rather than a 404, so \"no such encounter\" and \"encounter with no participants\" are indistinguishable here; the listing is unpaginated with no limit or cursor; the query keys on encounter_id alone with no tenant predicate, so cross-tenant reads are possible for a caller who knows the id.",
        "operationId": "get_api_encounters_encounter_id_participants",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "participant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Add a participant persona to the encounter",
        "description": "Adds a persona to an encounter as a participant with a role, optionally flagged required. The insert is an upsert on (encounter_id, persona_id, role) that clears left_at on conflict, so re-adding a participant who previously left rejoins them rather than erroring. Also performs a sdk-data-rights residency touch recording that the persona's data now lives in this app pool. QA edge cases: because of the ON CONFLICT the call IS idempotent per (encounter, persona, role) — repeat POSTs return 201 with the same participant_id, so tests must not expect a duplicate-key 409; the same persona added under a DIFFERENT role creates a second, separate participant row; the handler validates only persona_id and role, and never checks that the encounter exists or that its state still allows joins — an unknown encounter_id hits the foreign key and surfaces as an uncaught Fastify 500, and adding a participant to an already-closed or sealed encounter is accepted with 201; required defaults to false when omitted, which matters because only required participants can later block a close; the residency touch is try/catch-wrapped and non-fatal.",
        "operationId": "post_api_encounters_encounter_id_participants",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "participant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role": "attendee",
                    "required": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — <database error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persona_id": {
                    "type": "string"
                  },
                  "role": {
                    "type": "string"
                  },
                  "required": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "persona_id": "<id from personas>",
                "role": "attendee",
                "required": false
              }
            }
          }
        }
      }
    },
    "/api/encounters/{encounter_id}/transition": {
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Transition the encounter open -> in-progress",
        "description": "Drives the encounter state machine. Legal transitions are open -> in-progress|closed|sealed, in-progress -> closed|sealed, closed -> sealed, and sealed -> nothing (terminal). Closing or sealing first checks that no participant marked required has already left. Sealing additionally stamps sealed_at (plus closed_at if not already set) and cryptographically shreds the per-encounter Vault key. QA edge cases: the 400/409 split is the thing to get right — a `to` value outside the four known states is a 400 ValidationError, while a syntactically valid but illegal move (e.g. sealed -> open, or closed -> in-progress) is a 409 InvalidTransition; a nonexistent encounter_id is ALSO a 409, not a 404, because transitionEncounter throws 'Encounter <id> not found' inside the try block; blocking a close because a required participant left surfaces as the same 409 with a different details message, so assert on details; sealing is destructive and irreversible — re-sealing an already-sealed encounter is a 409 since sealed has no legal successors; the shredKey call is try/catch-wrapped and only logs, so a 200 seal response does not prove the key was actually shredded.",
        "operationId": "post_api_encounters_encounter_id_transition",
        "parameters": [
          {
            "name": "encounter_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "encounter_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to": "in-progress",
                    "actor_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid target state"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "InvalidTransition — Encounter <encounter_id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "to": "in-progress",
                "actor_id": "<id from personas>"
              }
            }
          }
        }
      }
    },
    "/api/encounters": {
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Open an encounter (issues per-encounter Vault key)",
        "description": "Opens an encounter (FR-EN-1 / FR-EN-3). The handler issues a per-encounter Vault key FIRST — parented to parent_key_id, tagged with the caller's region — and only then inserts the engagement.encounter row, so a Vault failure leaves no orphaned encounter. Returns 201 with the encounter including its vault_key_ref, state 'open', and retention_policy defaulting to 'default-7y'. Emits engagement.encounter.opened.v1 to the audit ledger (regulated retention). QA edge cases: tenant_id, kind, parent_key_id and region are all required and their absence is a single generic 400 'missing fields' with no per-field detail; parent_key_id is NOT validated by the route — an unknown or non-parent key makes issueKey throw, which is unhandled in the handler and surfaces as a Fastify 500 rather than a 400/404, which is the trap for QA; parent_encounter_id, address_id and billing_ref are optional passthroughs with no existence check, so a bogus parent_encounter_id either violates the FK (500) or is accepted silently depending on schema constraints; the call is not idempotent — every POST mints a new encounter AND a new Vault key; the audit emit is try/catch-wrapped and non-fatal, so a 201 does not prove the audit entry landed.",
        "operationId": "post_api_encounters",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "visit",
                    "retention_policy": "default-7y",
                    "region": "us-east-1",
                    "parent_key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "parent_encounter_id": null,
                    "address_id": null,
                    "billing_ref": "billing-ref-001",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — <Vault or database error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "retention_policy": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "parent_key_id": {
                    "type": "string"
                  },
                  "parent_encounter_id": {
                    "nullable": true
                  },
                  "address_id": {
                    "nullable": true
                  },
                  "billing_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "kind": "visit",
                "retention_policy": "default-7y",
                "region": "us-east-1",
                "parent_key_id": "<id from vault>",
                "parent_encounter_id": null,
                "address_id": null,
                "billing_ref": "billing-ref-001"
              }
            }
          }
        }
      }
    },
    "/api/events/checkin": {
      "post": {
        "tags": [
          "sdk-event"
        ],
        "summary": "Check in an issued ticket via its qr_token",
        "description": "Checks a ticket in at the door by its qr_token, atomically transitioning the ticket issued -> used and inserting a checkin row; a UNIQUE constraint on checkin.ticket_id backstops double check-in. qr_token and checked_in_by_persona_id are required; device_uuid is optional and stored as null when absent. Edge cases: missing required fields -> 400; an unknown token, a ticket that was never issued, and a ticket already checked in all collapse into the same 409 CannotCheckIn with 'Ticket not found, not issued, or already used' - so a duplicate scan is a 409, never a silent success; if the parent session row cannot be read the emitted audit records tenant 'unknown' but the check-in still succeeds.",
        "operationId": "post_api_events_checkin",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "checkin_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "qr_token": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "checked_in_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "gate-scanner-01",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "CannotCheckIn — Ticket not found, not issued, or already used"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "qr_token": {
                    "type": "string"
                  },
                  "checked_in_by_persona_id": {
                    "type": "string"
                  },
                  "device_uuid": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "qr_token": "<id from events>",
                "checked_in_by_persona_id": "<id from personas>",
                "device_uuid": "gate-scanner-01"
              }
            }
          }
        }
      }
    },
    "/api/events/sessions": {
      "post": {
        "tags": [
          "sdk-event"
        ],
        "summary": "Create an event session under an encounter",
        "description": "Opens an event session (a schedulable, ticketable occurrence) on an encounter. tenant_id, encounter_id, title, capacity, starts_at and ends_at are all required - note capacity is checked with `== null`, so 0 is accepted and immediately makes the session unsellable; address_id is optional. Edge cases: any missing required field -> 400; starts_at/ends_at are passed straight to `new Date()` with no ordering or validity check, so an inverted or unparseable range is stored rather than rejected; a negative capacity is not rejected at the route; an unknown encounter_id fails the FK inside the insert and surfaces as an unhandled 500; sold_count starts at 0 and the session emits event.session.opened.v1 (audit failures are swallowed).",
        "operationId": "post_api_events_sessions",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "title": "Acme QA Sample",
                    "address_id": null,
                    "capacity": 100,
                    "starts_at": "2026-09-01T10:00:00Z",
                    "ends_at": "2026-09-01T12:00:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "encounter_id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "address_id": {
                    "nullable": true
                  },
                  "capacity": {
                    "type": "integer"
                  },
                  "starts_at": {
                    "type": "string"
                  },
                  "ends_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "encounter_id": "<id from encounters>",
                "title": "<generated name>",
                "address_id": null,
                "capacity": 100,
                "starts_at": "2026-09-01T10:00:00Z",
                "ends_at": "2026-09-01T12:00:00Z"
              }
            }
          }
        }
      }
    },
    "/api/events/sessions/{session_id}": {
      "get": {
        "tags": [
          "sdk-event"
        ],
        "summary": "Fetch a session by id",
        "description": "Fetches one event session with its capacity and current sold_count - the pair QA needs to verify oversell protection. Edge cases: an unknown session_id returns 404; the route applies no tenant filter of its own beyond the JWT gate, so cross-tenant readability should be verified; a malformed (non-UUID) session_id fails the uuid cast and surfaces as an unhandled 500 rather than a 404.",
        "operationId": "get_api_events_sessions_session_id",
        "parameters": [
          {
            "name": "session_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "session_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/events/tickets": {
      "post": {
        "tags": [
          "sdk-event"
        ],
        "summary": "Issue a ticket for a session",
        "description": "Issues a ticket for a session and returns its generated qr_token. session_id and holder_persona_id are required; price is optional. Capacity is reserved atomically by a conditional UPDATE (sold_count < capacity AND status IN ('scheduled','live')) that row-locks the session, so concurrent buyers cannot oversell. Edge cases: missing required fields -> 400; a sold-out session, a cancelled/ended session, and a session_id that does not exist are indistinguishable to the client - all three return the same 409 CannotIssueTicket with 'Session sold out, cancelled, or not found'; nothing stops the same persona holding multiple tickets for one session, so the endpoint is not idempotent and each retry consumes another seat.",
        "operationId": "post_api_events_tickets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ticket_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "session_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "holder_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "price": 50,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "CannotIssueTicket — Session sold out, cancelled, or not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "session_id": {
                    "type": "string"
                  },
                  "holder_persona_id": {
                    "type": "string"
                  },
                  "price": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "session_id": "<id from events>",
                "holder_persona_id": "<id from personas>",
                "price": 50
              }
            }
          }
        }
      }
    },
    "/api/events/types": {
      "get": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "List the platform baseline plus this tenant's own event types",
        "description": "Lists every event type the caller may emit - the compile-time EVENT_TYPE_REGISTRY platform baseline plus the types this tenant has registered itself - with metadata, a total count and separate platform_count/tenant_count. Edge cases: the baseline half is static compile-time data, so it is identical on every call and never empty in a correctly built image, while the tenant half is a DB read scoped to the JWT's tenant_id claim and is empty for a tenant that has registered nothing; a token with no tenant_id claim gets the baseline only, never another tenant's types; the route takes no parameters and no pagination; the handler declares no requireAuth of its own, so its 401 comes from the gateway default-deny auth gate (the path is not on the public allowlist).",
        "operationId": "get_api_events_types",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": []
      },
      "post": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "Register a tenant event type",
        "description": "Registers ONE event type for the caller's own tenant, extending the compile-time EVENT_TYPE_REGISTRY baseline so a consuming application can emit its own audit events without a platform release. The tenant comes from the verified JWT claim and is never read from the body, so a user of tenant A cannot define vocabulary inside tenant B. Resolution at append time is baseline-first, then this tenant's rows, so a registration can never shadow or redefine a platform type. Edge cases: registering an event_type that already exists for this tenant is ADDITIVE, returning 200 with the STORED metadata and created:false rather than overwriting it (a boot-time provisioner re-running on every deploy is the expected caller); an event_type that collides with a platform baseline name is rejected 400 rather than accepted as a shadow; a name that does not match <domain>.<entity>.<verb>.v<N> is rejected 400 at REGISTRATION, where the author can still fix it, rather than at append time in production; schema_state defaults to 'active', compaction_policy to 'none' and schema_version to 1 when omitted; a token carrying no tenant_id claim is rejected 400 because the row has nowhere to land. IDEMPOTENCY (MUST-47): the primary key is (tenant_id, event_type) and tenant_id comes from a tenant created fresh by the signup-tenant dependency on every run, so the fixed event_type below cannot collide across runs. NOTE ON fieldEnums (MUST-39): event_type is deliberately absent from fieldEnums because it is NOT enum-backed - it is pattern-validated against EVENT_TYPE_NAME_PATTERN (<domain>.<entity>.<verb>.v<N>) and any conforming name is valid, so publishing a finite list would tell QA something false about what the field accepts. The four metadata fields below ARE enum-backed (Postgres CHECK-in-list) and are enumerated.",
        "operationId": "post_api_events_types",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — details[]: a tenant-scoped token is required to register an event type"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string"
                  },
                  "retention_class": {
                    "type": "string"
                  },
                  "conflict_policy": {
                    "type": "string"
                  },
                  "schema_state": {
                    "type": "string"
                  },
                  "compaction_policy": {
                    "type": "string"
                  },
                  "schema_version": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "event_type": "capture.lead.created.v1",
                "retention_class": "regulated",
                "conflict_policy": "event-sourcing",
                "schema_state": "active",
                "compaction_policy": "none",
                "schema_version": 1
              }
            }
          }
        }
      }
    },
    "/api/events/types/{type}": {
      "get": {
        "tags": [
          "sdk-audit"
        ],
        "summary": "Look up one event type, platform baseline first then tenant",
        "description": "Looks up one event type by its exact key and returns its metadata plus a 'source' of 'platform' or 'tenant'. The compile-time EVENT_TYPE_REGISTRY baseline is checked FIRST and the caller's tenant-registered types second, which is the property that stops a tenant registration shadowing a platform type. Edge cases: the lookup is an exact, case-sensitive key match, so a versionless or mis-cased name (e.g. 'event.ticket.issued' instead of 'event.ticket.issued.v1') returns 404 UnregisteredEventType echoing the requested type and pointing at POST /api/events/types; a name registered by ANOTHER tenant also 404s, since only the caller's own tenant rows are consulted; the baseline half never varies per tenant or over the life of the process; the handler declares no requireAuth of its own, so its 401 comes from the gateway default-deny auth gate.",
        "operationId": "get_api_events_types_type",
        "parameters": [
          {
            "name": "type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "type path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "UnregisteredEventType — event_type '<type>' is not registered"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/evidence/capture": {
      "get": {
        "tags": [
          "sdk-evidence"
        ],
        "summary": "List captures for an encounter",
        "description": "Lists every evidence capture belonging to one encounter, selected by the required encounter_id query param. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: encounter_id is mandatory and its absence (or an empty value) is a 400, since the route would otherwise scan unbounded; an unknown but well-formed encounter_id returns 200 with an empty data array rather than a 404; there is no limit/offset paging, so an encounter with many captures returns the whole set in one payload; the caller JWT tenant is not compared to the rows, so captures on another tenant encounter are readable - verify tenant scoping deliberately; a non-UUID encounter_id fails the uuid cast in an untried service call and surfaces as a Fastify 500, not a 400.",
        "operationId": "get_api_evidence_capture",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — encounter_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-evidence"
        ],
        "summary": "Provenance-stamped capture intake for an open encounter",
        "description": "Provenance-stamped evidence intake: records a capture (device, attestation, blob id + checksum, capture time, consent ref and optional lat/lng/altitude/IMU signature) against an encounter and returns 201. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: nine fields are required (tenant_id, encounter_id, capturer_persona_id, device_uuid, device_attestation_id, raw_blob_id, blob_checksum, consent_ref, captured_at) and the 400 names every missing one in a single message; the geo/IMU fields default to null when omitted; if the encounter was sealed between upload and intake the write is refused with 409 encounter_sealed carrying encounter_id and sealed_at - the canonical AC-11 path; every other service failure (unknown encounter foreign key, bad checksum, malformed captured_at, non-UUID identifiers, retention-class violations) is flattened into a 400 with the raw error message rather than a 404 or 500, so a missing FK looks like a validation error; tenant_id comes from the body, not the JWT, so cross-tenant capture is not blocked here.",
        "operationId": "post_api_evidence_capture",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capturer_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_attestation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "raw_blob_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "blob_checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                    "captured_at": "2026-01-15T10:30:00Z",
                    "lat": 37.7749,
                    "lng": -122.4194,
                    "altitude": 12.5,
                    "imu_signature": "aW11LXNpZ25hdHVyZS1zYW1wbGU=",
                    "consent_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "retention_class": "regulated",
                    "retention_expires_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields: <comma-separated list>"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "encounter_sealed — encounter <encounter_id> is sealed (at <sealed_at>) - no new evidence captures may reference it"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "encounter_id": {
                    "type": "string"
                  },
                  "capturer_persona_id": {
                    "type": "string"
                  },
                  "device_uuid": {
                    "type": "string"
                  },
                  "device_attestation_id": {
                    "type": "string"
                  },
                  "raw_blob_id": {
                    "type": "string"
                  },
                  "blob_checksum": {
                    "type": "string"
                  },
                  "captured_at": {
                    "type": "string"
                  },
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "altitude": {
                    "type": "number"
                  },
                  "imu_signature": {
                    "type": "string"
                  },
                  "consent_ref": {
                    "type": "string"
                  },
                  "retention_class": {
                    "type": "string"
                  },
                  "retention_expires_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "encounter_id": "<id from encounters>",
                "capturer_persona_id": "<id from personas>",
                "device_uuid": "<id from devices>",
                "device_attestation_id": "<id from devices>",
                "raw_blob_id": "<id from media>",
                "blob_checksum": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                "captured_at": "<generated pastdatetime>",
                "lat": 37.7749,
                "lng": -122.4194,
                "altitude": 12.5,
                "imu_signature": "aW11LXNpZ25hdHVyZS1zYW1wbGU=",
                "consent_ref": "<id from consents>",
                "retention_class": "regulated",
                "retention_expires_at": "<generated futuredatetime>"
              }
            }
          }
        }
      }
    },
    "/api/evidence/capture/{id}": {
      "get": {
        "tags": [
          "sdk-evidence"
        ],
        "summary": "Fetch a capture by id",
        "description": "Fetches a single evidence capture row by its id path param. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: the lookup is by id alone and the caller JWT tenant is never compared to the row tenant_id, so any authenticated caller can read any capture - tenant scoping must be tested explicitly; a well-formed but unknown id returns 404 {success:false,error:\"not found\"}, while a malformed non-UUID id fails the uuid cast inside an untried service call and surfaces as a Fastify 500 instead; the handler does not filter sealed or retention-expired captures, so those still return 200.",
        "operationId": "get_api_evidence_capture_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — not found"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/flags/{flag_id}/evaluate": {
      "post": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "Evaluate flag for a tenant",
        "description": "Evaluates a flag for an evaluation context (tenant_id, persona_id, bu_id, arbitrary attributes) and returns resolved_value, matched_rollout_id and kill_switch_engaged. Resolution order: kill switch first, then active rollouts ordered tenant-specific-first by ascending priority whose predicate matches and whose deterministic percentage bucket (hashed on persona_id, else tenant_id, else 'anon') falls under rollout_percent, then the flag's default_value. Edge cases: the entire body is optional and an unknown flag_id does NOT 404 - it fails open with 200 and resolved_value null, matched_rollout_id null, kill_switch_engaged false, so tests must assert the body rather than the status; percentage bucketing is stable per (flag, subject), so repeated calls for the same subject return the same answer; every evaluation is sampled for telemetry.",
        "operationId": "post_api_flags_flag_id_evaluate",
        "parameters": [
          {
            "name": "flag_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "flag_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "{{var:persona_id}}",
                    "bu_id": "{{var:bu_id}}",
                    "attributes": {},
                    "evaluate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "bu_id": {
                    "type": "string"
                  },
                  "attributes": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "{{var:persona_id}}",
                "bu_id": "{{var:bu_id}}",
                "attributes": {}
              }
            }
          }
        }
      }
    },
    "/api/flags/{flag_id}": {
      "get": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "Get flag",
        "description": "Fetches a single feature-flag definition by flag_id. Edge cases: an unknown flag_id returns 404 (in contrast to the evaluate endpoint, which answers 200 with a null value for an unknown flag - a deliberate fail-open asymmetry QA should verify); flag_id is a caller-chosen string key, not a UUID, so lookups are exact and case-sensitive; the response carries the definition only, not the flag's rollout rules.",
        "operationId": "get_api_flags_flag_id",
        "parameters": [
          {
            "name": "flag_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "flag_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "flag_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/flags/{flag_id}/kill-switch": {
      "post": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "Engage kill switch",
        "description": "Engages or releases a flag's kill switch. body.engaged is required and must be a real boolean - the check is `typeof !== 'boolean'`, so the strings \"true\"/\"false\", 0/1 and null are all rejected 400. Edge cases: an unknown flag_id -> 404; setting the switch to the value it already holds is accepted and the call is idempotent; while engaged, evaluate() short-circuits every rollout and returns the type-appropriate off value (false for boolean, 0 for numeric, null otherwise) with kill_switch_engaged true, so QA should assert the evaluate response shape immediately after flipping this.",
        "operationId": "post_api_flags_flag_id_kill_switch",
        "parameters": [
          {
            "name": "flag_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "flag_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "kill_switch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "engaged": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing engaged"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "engaged": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "engaged": true
              }
            }
          }
        }
      }
    },
    "/api/flags/{flag_id}/rollouts": {
      "post": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "Tenant-scoped rollout",
        "description": "Creates or updates a rollout rule for a flag: an optional tenant_id (null means the rule applies to every tenant), a predicate matched against the evaluation context, the value to serve, a priority (lower wins, evaluated tenant-specific-first) and an active flag. Only value is required, and it is checked with `=== undefined`, so an explicit null IS a valid value. Edge cases: omitting value -> 400; an unknown flag_id is not verified at this route, so a rollout can be attached to a flag that does not exist; predicate contents are not schema-validated; two rules with the same priority resolve in an unspecified order; the response is 201 even when the call updated an existing rollout.",
        "operationId": "post_api_flags_flag_id_rollouts",
        "parameters": [
          {
            "name": "flag_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "flag_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "rollout_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "predicate": {},
                    "value": true,
                    "priority": 100,
                    "active": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing value"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "predicate": {
                    "type": "object",
                    "properties": {}
                  },
                  "value": {
                    "type": "boolean"
                  },
                  "priority": {
                    "type": "integer"
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "predicate": {},
                "value": true,
                "priority": 100,
                "active": true
              }
            }
          }
        }
      }
    },
    "/api/flags": {
      "get": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "List flags",
        "description": "Lists every registered feature flag with its kind, default_value and kill_switch state. Edge cases: the route accepts no filter or pagination params and is not tenant-scoped - flag definitions are global, so every authenticated caller sees the full catalogue; an empty registry returns 200 with an empty array rather than a 404; rollouts are not included in this response, only the flag definitions.",
        "operationId": "get_api_flags",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "flag_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "tags": [
          "sdk-feature-flags"
        ],
        "summary": "Upsert agent kill-switch flag",
        "description": "Creates or updates (upserts) a feature flag definition keyed by flag_id, with an optional description, kind, default_value, kill_switch and schema_ref. kind, when supplied, must be one of boolean, variant, numeric or json. Edge cases: a missing flag_id -> 400; an unrecognised kind -> 400, but omitting kind entirely is allowed and leaves the existing/default kind in place; default_value is not validated against kind or schema_ref, so a boolean flag can be given a string default; the operation is a true upsert and therefore idempotent, and it answers 200 (not 201) even when creating; flags are global rather than tenant-scoped - per-tenant behaviour is expressed through rollouts.",
        "operationId": "put_api_flags",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "flag_id": "agent.cost-steward.enabled",
                    "description": "Per-agent kill switch",
                    "kind": "boolean",
                    "default_value": true,
                    "kill_switch": false,
                    "schema_ref": null,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing flag_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "flag_id": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "default_value": {
                    "type": "boolean"
                  },
                  "kill_switch": {
                    "type": "boolean"
                  },
                  "schema_ref": {
                    "nullable": true
                  }
                }
              },
              "example": {
                "flag_id": "agent.cost-steward.enabled",
                "description": "Per-agent kill switch",
                "kind": "boolean",
                "default_value": true,
                "kill_switch": false,
                "schema_ref": null
              }
            }
          }
        }
      }
    },
    "/api/geo/addresses/{address_id}": {
      "get": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Read by canonical id",
        "description": "Reads one canonical address row by address_id and returns 200 with { data: { address } }. Read-only, requires a valid JWT; the record is not tenant-scoped (geo.address is a platform-wide canonical table), so any authenticated caller can read any address_id. Edge cases: unknown but well-formed UUID returns 404, an address_id that was consumed as the loser of POST /api/geo/merge is deleted and therefore also 404s afterwards, and a non-UUID address_id reaches the Postgres UUID cast unguarded (22P02) and surfaces as a 500.",
        "operationId": "get_api_geo_addresses_address_id",
        "parameters": [
          {
            "name": "address_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "address_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "address_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/geo/bbox-query": {
      "post": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Query bbox around SF",
        "description": "Returns the canonical addresses whose geometry falls inside the supplied bounding box, using PostGIS ST_Intersects when the extension is available and silently falling back to a plain lat/lng BETWEEN range scan when it is not. All four coordinates must be JSON numbers. Edge cases: the caller-supplied limit is clamped to a maximum of 1000 and defaults to 100, so an oversized or absent limit never returns more than 1000 rows; an inverted or zero-area box returns an empty array with 200, not an error; coordinates sent as strings fail the strict typeof guard; addresses with NULL lat/lng are excluded by the fallback path; requires a valid JWT.",
        "operationId": "post_api_geo_bbox_query",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "bbox_query_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "min_lat": 37.7,
                    "min_lng": -122.5,
                    "max_lat": 37.8,
                    "max_lng": -122.4,
                    "limit": 10,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing bbox coords"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "min_lat": {
                    "type": "number"
                  },
                  "min_lng": {
                    "type": "number"
                  },
                  "max_lat": {
                    "type": "number"
                  },
                  "max_lng": {
                    "type": "number"
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "min_lat": 37.7,
                "min_lng": -122.5,
                "max_lat": 37.8,
                "max_lng": -122.4,
                "limit": 10
              }
            }
          }
        }
      }
    },
    "/api/geo/canonicalize": {
      "post": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Canonicalize a US address",
        "description": "Canonicalizes a raw address string into a deduplicated geo.address row and records a geo.address_alias for the raw input, returning 200 with the canonical address. raw_input, street, city and country are all mandatory; the alias hash is looked up first, so re-posting the same raw_input is idempotent and returns the existing address rather than creating a duplicate. Edge cases: repeated/duplicate raw_input (alias hit, no new row, no audit event), a different raw_input that normalizes to the same (street, city, country, postal_code) hash (sibling reuse - same address_id), empty-string street/city/country (rejected as missing), optional lat/lng/region/postal_code/geo_node_id/provider_refs omitted, a geo_node_id that does not exist (FK violation surfaces as an unhandled 500), and missing/expired JWT.",
        "operationId": "post_api_geo_canonicalize",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "raw_input": "123 Main St, Cityville, US",
                    "street": "123 Main St",
                    "city": "Cityville",
                    "region": "CA",
                    "postal_code": "94110",
                    "country": "US",
                    "lat": 37.7749,
                    "lng": -122.4194,
                    "geo_node_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "provider_refs": {
                      "mapbox": "poi.a1b2c3"
                    },
                    "canonicalize_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "raw_input": {
                    "type": "string"
                  },
                  "street": {
                    "type": "string"
                  },
                  "city": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "postal_code": {
                    "type": "string"
                  },
                  "country": {
                    "type": "string"
                  },
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "geo_node_id": {
                    "type": "string"
                  },
                  "provider_refs": {
                    "type": "object",
                    "properties": {
                      "mapbox": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "raw_input": "123 Main St, Cityville, US",
                "street": "123 Main St",
                "city": "Cityville",
                "region": "CA",
                "postal_code": "94110",
                "country": "US",
                "lat": 37.7749,
                "lng": -122.4194,
                "geo_node_id": "<id from geo-nodes>",
                "provider_refs": {
                  "mapbox": "poi.a1b2c3"
                }
              }
            }
          }
        }
      }
    },
    "/api/geo/geocode": {
      "post": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Geocode raw input",
        "description": "Forward-geocodes a free-text address via the active geo provider and then canonicalizes the enriched result, returning 200 with the canonical address (or { address: null } when the provider yields nothing). Only raw_input is required. Edge cases: raw_input missing or empty string is rejected with 400; a provider miss returns 200 with a null address rather than a 404; on a provider hit the degraded canonicalize path stores raw_input as the street with city and country literals of \"?\", so repeated geocodes of the same raw_input hit the alias cache and are idempotent; requires a valid JWT.",
        "operationId": "post_api_geo_geocode",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "geocode_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "raw_input": "123 Main St, Cityville, US",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing raw_input"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "raw_input": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "raw_input": "123 Main St, Cityville, US"
              }
            }
          }
        }
      }
    },
    "/api/geo/merge": {
      "post": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Merge two addresses",
        "description": "Merges two canonical addresses: every alias of the loser is re-pointed at the winner, a geo.merge_event audit row is written, and the loser geo.address row is deleted. Both ids must be well-formed UUIDs - the handler pre-validates the format so a malformed id cannot reach Postgres. Edge cases: a syntactically valid but non-existent winner or loser trips the merge_event foreign key (23503), which the handler maps to 404; passing the same id as winner and loser is not blocked and self-deletes the row; the operation is destructive and NOT idempotent - replaying the same merge 404s the second time because the loser no longer exists; operator_id is optional and defaults to null; requires a valid JWT.",
        "operationId": "post_api_geo_merge",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "merge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "winner_address_id": "{{var:geo_winner_address_id}}",
                    "loser_address_id": "{{var:geo_loser_address_id}}",
                    "operator_id": "{{var:operator_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — winner or loser address not found"
          },
          "500": {
            "description": "InternalError — <underlying error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "winner_address_id": {
                    "type": "string"
                  },
                  "loser_address_id": {
                    "type": "string"
                  },
                  "operator_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "winner_address_id": "{{var:geo_winner_address_id}}",
                "loser_address_id": "{{var:geo_loser_address_id}}",
                "operator_id": "{{var:operator_id}}"
              }
            }
          }
        }
      }
    },
    "/api/geo/reverse-geocode": {
      "post": {
        "tags": [
          "sdk-geo"
        ],
        "summary": "Reverse-geocode SF",
        "description": "Reverse-geocodes a lat/lng pair through the active geo provider and returns 200 with { street, city, country } or null when the provider has no match. Both lat and lng must be JSON numbers - the guard is a strict typeof check, so numeric strings such as \"12.97\" are rejected with 400. Edge cases: lat/lng sent as strings, either coordinate missing, lat/lng of 0 (valid - 0 is a number and passes the guard), out-of-range coordinates (not validated here; passed to the provider, which returns null), a provider miss returning 200 with a null address, and missing/expired JWT.",
        "operationId": "post_api_geo_reverse_geocode",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reverse_geocode_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "lat": 37.7749,
                    "lng": -122.4194,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing lat/lng"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  }
                }
              },
              "example": {
                "lat": 37.7749,
                "lng": -122.4194
              }
            }
          }
        }
      }
    },
    "/api/geo-nodes": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Create a US region geo node",
        "description": "Creates a node in the tenant.geo_node residency tree (region > country > state > city > locality), used to place tenants for data-residency separately from pool_index. Requires name and a kind from the CHECK-constrained enum; residency_class defaults to 'open', code and parent_geo_node_id are optional. QA edge cases: the validator only rejects a missing/empty name, an out-of-enum kind, and an out-of-enum residency_class — everything else falls through to Postgres. There is NO unique constraint on (kind, code) or name, so re-POSTing the same payload creates a second distinct geo_node_id (non-idempotent, duplicates are silently allowed). A parent_geo_node_id that does not exist violates the self-referencing FK and is remapped to 400 ValidationError (not 404). A parent_geo_node_id that is not a valid UUID string yields 500 InternalError, since the uncaught() helper only special-cases 'violates foreign key' / 'duplicate key' / 'not found' text. Oversized name/code have no length cap (TEXT columns). The route is NOT tenant-scoped: geo_node rows are platform-global, so any valid tenant JWT can create nodes visible to every tenant; only the presence of a valid JWT is checked, never a role or ADMIN_OPS_TOKEN.",
        "operationId": "post_api_geo_nodes",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "geo_node_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "region",
                    "code": "us-east-1",
                    "name": "Acme QA Sample",
                    "residency_class": "open",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — duplicate key value violates unique constraint"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "residency_class": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "kind": "region",
                "code": "us-east-1",
                "name": "<generated name>",
                "residency_class": "open"
              }
            }
          }
        }
      }
    },
    "/api/grants/{grant_id}/revoke": {
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "Revoke an active encounter grant",
        "description": "Revokes an encounter grant (FR-EN-5) by stamping engagement.encounter_grant.revoked_at = now(), returning 200 with the revoked grant (grant_id, encounter_id, grantee/issuer persona ids, scope, issued_at, expires_at, revoked_at, capability_token_ref). The UPDATE is guarded by `revoked_at IS NULL`. Edge cases: a second revoke of the same grant matches zero rows and returns 404 (not a silent 200), as does an unknown grant_id; a grant that has merely EXPIRED is still revocable because expiry is not part of the WHERE clause, while an already-revoked one is not; a non-UUID grant_id fails the Postgres cast and surfaces as an uncaught 500. Requires a valid tenant JWT.",
        "operationId": "post_api_grants_grant_id_revoke",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/handoffs": {
      "get": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "List handoffs for the signup tenant",
        "description": "List handoffs for a tenant, most-recently-updated first. Tenant-scoped via the required tenant_id query param; optionally filtered by status and/or deal_id, with limit/offset paging (defaults 50/0).",
        "operationId": "get_api_handoffs",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "handoff_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Create a draft handoff for the signup tenant",
        "description": "Create a Sales->Delivery handoff record in status 'draft'. tenant_id and from_persona_id (the sales persona handing off) are required; deal_id (loose ref to crm.deal), cs_owner/backup personas, kickoff_ref, and the prework/promises/risks/integrations/milestones JSONB arrays are optional. Emits handoff.created.v1. The record then moves through the lifecycle via POST /api/handoffs/:handoff_id/transition.",
        "operationId": "post_api_handoffs",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "handoff_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "from_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kickoff_ref": "kickoff-2026-Q3",
                    "promises": [
                      "go-live in 30 days"
                    ],
                    "risks": [
                      "data migration scope unknown"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and from_persona_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "from_persona_id": {
                    "type": "string"
                  },
                  "kickoff_ref": {
                    "type": "string"
                  },
                  "promises": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "risks": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "from_persona_id": "<id from auth>",
                "kickoff_ref": "kickoff-2026-Q3",
                "promises": [
                  "go-live in 30 days"
                ],
                "risks": [
                  "data migration scope unknown"
                ]
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{handoff_id}/approval/decision": {
      "post": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "CS approves the handoff (pending -> accepted)",
        "description": "Record the sdk-approval outcome against a handoff, mapping the approval decision onto the handoff lifecycle: 'approved' moves pending -> accepted (delivery now owns the engagement), 'rejected' moves pending -> rejected and records reject_reason. The move runs through the standard handoff transition path, so it is validated against HANDOFF_TRANSITIONS, stamps accepted_at/rejected_at, and emits the lifecycle event (handoff.accepted.v1 / handoff.rejected.v1) via sdk-audit. Edge cases: the decision is only legal from 'pending' — deciding on a draft handoff (approval never requested) or on an already-decided/terminal one returns 409; 'rejected' is terminal, so a rejected handoff cannot later be accepted; reject_reason is persisted only when decision=rejected and ignored otherwise; 400 when tenant_id or decision is missing, or decision is outside approved|rejected; 404 when the handoff does not exist for that tenant.",
        "operationId": "post_api_handoffs_handoff_id_approval_decision",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approved",
                    "reject_reason": "not applicable when approving",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and decision are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — invalid transition <from> -> accepted|rejected"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "decision": {
                    "type": "string"
                  },
                  "reject_reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "decision": "approved",
                "reject_reason": "not applicable when approving"
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{handoff_id}/approval/request": {
      "post": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "File the CS approval for the submitted handoff",
        "description": "File the CS accept/reject approval for a Sales->Delivery handoff and submit it for review. The gate itself is delegated to sdk-approval — no new approval engine is built in sdk-handoff: the pluggable creator files an approval.request whose subject is the handoff (subject_kind 'handoff.handoff') and the returned request id is stored in handoff.approval_id. A handoff in 'draft' is moved to 'pending' as part of the request; a handoff already past draft is left in its current state, so re-requesting is IDEMPOTENT and returns the existing record rather than 409ing. When no handoff approval route is configured on the gateway (HANDOFF_APPROVAL_ROUTE_ID unset) the SDK's default creator mints a synthetic UUID ref, so the happy path needs no seeded approval route. Edge cases: 400 when tenant_id is absent from the body; 404 when the handoff_id does not exist for that tenant (tenant-scoped — another tenant's handoff reads as not-found); 409 only if the underlying draft->pending transition is rejected from a terminal state (completed/cancelled/rejected).",
        "operationId": "post_api_handoffs_handoff_id_approval_request",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — invalid transition <from> -> pending"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{handoff_id}": {
      "get": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Get the created handoff",
        "description": "Fetch a single handoff by id, tenant-scoped via the required tenant_id query param. Returns the full record including lifecycle timestamps (submitted_at/accepted_at/rejected_at/completed_at). 404 when the handoff is not found for the tenant.",
        "operationId": "get_api_handoffs_handoff_id",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "handoff_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Enrich the draft handoff (owner + milestones)",
        "description": "Update editable fields (cs_owner/backup personas, kickoff_ref, prework/promises/risks/integrations/milestones, workflow_run_id, approval_id, metadata) while the handoff is still draft or pending. tenant_id is required. Emits handoff.updated.v1. Returns 409 NotEditable once the handoff is accepted/rejected/completed/cancelled, 404 if not found.",
        "operationId": "patch_api_handoffs_handoff_id",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "handoff_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "cs_owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "milestones": [
                      {
                        "name": "Kickoff",
                        "due": "2026-08-01"
                      }
                    ],
                    "metadata": {
                      "priority": "high"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "NotEditable — handoff in status '<status>' is no longer editable"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "cs_owner_persona_id": {
                    "type": "string"
                  },
                  "milestones": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "due": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "priority": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "cs_owner_persona_id": "<id from auth>",
                "milestones": [
                  {
                    "name": "Kickoff",
                    "due": "2026-08-01"
                  }
                ],
                "metadata": {
                  "priority": "high"
                }
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{handoff_id}/saga": {
      "get": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Read the saga projection after starting it",
        "description": "List the per-phase saga projection for a handoff (handoff.saga_step rows): each row is one saga phase (kickoff/prework/promises/risks/milestones) with its status ('done' once the sdk-workflow step succeeded, 'compensated' if a later failure rolled it back), the driving workflow run_id and created_at. Returns an empty list if the saga has not been started. tenant_id query param is required.",
        "operationId": "get_api_handoffs_handoff_id_saga",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "saga_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/handoffs/{handoff_id}/saga/start": {
      "post": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Start the saga for the created handoff",
        "description": "Start the Sales->Delivery handoff saga for a handoff. Drives the kickoff -> prework -> promises -> risks -> milestones phases as a durable sdk-workflow saga (each phase is a typed sdk-workflow step with a registered compensator; on a step failure the prior steps' compensators run in reverse, flipping their handoff.saga_step projection to 'compensated'). No new workflow engine is introduced — registration, execution and durability come from sdk-workflow, and the definition ('handoff.saga') is registered at gateway boot. Returns 202 with the workflow run_id, run status and the ordered phase list. tenant_id is required; 404 if the handoff does not exist for the tenant.",
        "operationId": "post_api_handoffs_handoff_id_saga_start",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{handoff_id}/transition": {
      "post": {
        "tags": [
          "sdk-handoff"
        ],
        "summary": "Submit the draft handoff (draft -> pending)",
        "description": "Advance a handoff's status. Valid transitions: draft->pending (submit), pending->accepted|rejected, accepted->completed; a handoff may be cancelled from any non-terminal state. The matching lifecycle timestamp is stamped, reject_reason recorded when rejecting, and a lifecycle event emitted (handoff.submitted/accepted/rejected/completed/cancelled.v1). tenant_id and status are required. 409 InvalidTransition when the transition is not allowed from the current state; 404 if not found.",
        "operationId": "post_api_handoffs_handoff_id_transition",
        "parameters": [
          {
            "name": "handoff_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "handoff_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "pending",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and status are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — invalid transition <from> -> <to>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "status": "pending"
              }
            }
          }
        }
      }
    },
    "/api/hdk/camera/capabilities": {
      "get": {
        "tags": [
          "hdk-camera"
        ],
        "summary": "List native camera capabilities",
        "description": "Returns the hdk-camera native capability manifest — supported photo formats (jpeg/heic/raw_dng), video codecs (h264/hevc/av1), max photo (48MP) and video (4K@60) resolution, flash/depth-sensor/OCR-passthrough flags, and the iOS/Android native module names the JS bridge must load. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_camera_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/camera/recording-presets": {
      "get": {
        "tags": [
          "hdk-camera"
        ],
        "summary": "List camera recording presets",
        "description": "Returns the four hdk-camera video recording presets (standard 1080p30 h264, high 4K30 hevc, slow-mo 1080p240 h264, time-lapse 4K 1fps hevc) so the client can offer a preset picker without hard-coding resolutions. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_camera_recording_presets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "recording_preset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/image-editor/capabilities": {
      "get": {
        "tags": [
          "hdk-image-editor"
        ],
        "summary": "List native image-editor capabilities",
        "description": "Returns the hdk-image-editor capability manifest — available tools (crop/rotate/flip/markup/text/blur/sharpen/filter), the five named filters, supported output formats (jpeg/png/heic), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_image_editor_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/map/capabilities": {
      "get": {
        "tags": [
          "hdk-map"
        ],
        "summary": "List native map capabilities",
        "description": "Returns the hdk-map capability manifest — supported overlay types (pin/polyline/polygon/heatmap/cluster), gestures, offline-caching and geofencing flags, routing modes (driving/walking/cycling/transit), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_map_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/map/tile-providers": {
      "get": {
        "tags": [
          "hdk-map"
        ],
        "summary": "List map tile providers",
        "description": "Returns the four configured map tile providers (mapbox-streets, mapbox-satellite, apple-standard, osm) with their {z}/{x}/{y} URL patterns and a requires_token flag. Security-relevant assertion for QA: this endpoint must NEVER return an actual tile access token — clients fetch those separately via sdk-secrets, so a response containing a token-like value is a defect. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_map_tile_providers",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "tile_provider_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/measure": {
      "get": {
        "tags": [
          "hdk-measure"
        ],
        "summary": "List measurements for a capture",
        "description": "Lists every measurement recorded for one capture, filtered by the required capture_id query param and returned as { success: true, data: [...] }. QA edge cases: omitting capture_id is a 400, but an unknown or non-existent capture_id is NOT — it returns 200 with an empty array, so \"no such capture\" and \"capture with zero measurements\" are indistinguishable; there is no limit/offset parameter, so the endpoint returns the full unpaginated set for a capture; there is no tenant scoping on the query, so results are keyed purely by capture_id.",
        "operationId": "get_api_hdk_measure",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "measure_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — capture_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "hdk-measure"
        ],
        "summary": "Record an AR measurement",
        "description": "Records one HDK device measurement (area / distance / volume) against a capture and persists it to hdk_measure.measurement. The route validates capture_id, kind, value, unit and device_uuid at the edge, then measurementService re-validates and inserts, returning 201 with the stored row. QA edge cases: EVERY validation failure — route-level missing field, unknown kind, non-finite or negative value, a captured_at that is not valid ISO 8601, or a database insert failure — collapses to HTTP 400, so assert on the error string, not just the status; value === 0 is accepted (only negative is rejected) while value omitted entirely is rejected, so a 0-value test must not be treated as a missing-field test; tenant_id is optional and defaults to null, meaning this endpoint does NOT enforce tenant scoping on write; the call is not idempotent — POSTing the same body twice creates two rows with different ids; the route has no requireAuth preHandler of its own but is still covered by the gateway default-deny authGate, so an anonymous call returns 401 before reaching the handler.",
        "operationId": "post_api_hdk_measure",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "measure_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capture_id": "{{var:capture_id}}",
                    "kind": "distance",
                    "value": 3.5,
                    "unit": "m",
                    "accuracy_class": "high",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "captured_at": "2026-01-15T10:30:00Z",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — capture_id, kind, value, unit, device_uuid are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "capture_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "value": {
                    "type": "number"
                  },
                  "unit": {
                    "type": "string"
                  },
                  "accuracy_class": {
                    "type": "string"
                  },
                  "device_uuid": {
                    "type": "string"
                  },
                  "captured_at": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "capture_id": "{{var:capture_id}}",
                "kind": "distance",
                "value": 3.5,
                "unit": "m",
                "accuracy_class": "high",
                "device_uuid": "<generated uuid>",
                "captured_at": "<generated pastdatetime>",
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/hdk/measure/{id}": {
      "get": {
        "tags": [
          "hdk-measure"
        ],
        "summary": "Fetch one measurement by id",
        "description": "Fetches a single measurement from hdk_measure.measurement by its measurement id and returns it as { success: true, data }. QA edge cases: an id that does not exist returns 404 with error \"not found\" — the same 404 is returned for a well-formed-but-unknown UUID and for a syntactically invalid id, so a malformed-id test cannot be distinguished from a missing-record test by status alone; there is no tenant filter on the lookup, so any authenticated caller who knows an id can read that row (do not assert cross-tenant isolation here); the read is idempotent and safe to repeat.",
        "operationId": "get_api_hdk_measure_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "measure_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/scanner/capabilities": {
      "get": {
        "tags": [
          "hdk-scanner"
        ],
        "summary": "List native scanner capabilities",
        "description": "Returns the hdk-scanner capability manifest — supported barcode symbologies (qr_code, pdf_417, code_128, ean_13, data_matrix), document-detection and OCR flags, and the iOS/Android native module names, so JS can negotiate scanner availability without calling into native code. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_scanner_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/video-editor/capabilities": {
      "get": {
        "tags": [
          "hdk-video-editor"
        ],
        "summary": "List native video-editor capabilities",
        "description": "Returns the hdk-video-editor capability manifest — available tools (trim/merge/caption/mute/speed/overlay), output formats (mp4/hevc/webm), max resolution (4K), and the iOS/Android native module names. The handler is a pure static-metadata read: it takes no body, params or query, touches no database, and always returns HTTP 200 with the same JSON for every caller. QA edge cases: the route is behind requireAuth, so a missing/malformed Authorization header or an expired JWT returns 401 before the handler runs; there is no tenant scoping, so two different tenants receive byte-identical payloads (do not assert per-tenant differences); unknown query params and request bodies are ignored rather than rejected; there is no pagination, no idempotency concern (GET is naturally idempotent), and no 404/409 path because nothing is looked up.",
        "operationId": "get_api_hdk_video_editor_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk/watermark": {
      "get": {
        "tags": [
          "hdk-watermark"
        ],
        "summary": "List watermark applications for a variant",
        "description": "Lists every watermark application recorded against one media variant, filtered by the required variant_id query param. QA edge cases: a missing variant_id is a 400, but an unknown variant_id returns 200 with an empty array rather than a 404 — \"no such variant\" and \"variant with no watermarks\" are indistinguishable; the listing is unpaginated (no limit/offset), so a variant with many applications returns them all in one response; the query is keyed only by variant_id with no tenant predicate.",
        "operationId": "get_api_hdk_watermark",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "watermark_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — variant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "hdk-watermark"
        ],
        "summary": "Record a watermark application",
        "description": "Records a watermark application (visible / invisible / cryptographic) against a media variant, persisting the payload envelope as bytes in hdk_watermark.application and returning 201. payload_envelope is accepted as either base64 or plain utf8 — the service sniffs the string and decodes base64 only when it matches the base64 alphabet AND its length is a multiple of 4, otherwise it stores the raw utf8 bytes. QA edge cases: that sniffing is the subtle one — a short plain-text value that happens to be pure base64 characters with length %4 === 0 will be silently base64-decoded, so byte-length assertions must account for it; an envelope that decodes to zero bytes is rejected as empty even though the field was non-empty; oversized envelopes are rejected above HDK_WATERMARK_MAX_PAYLOAD_BYTES (default 16384 bytes, measured AFTER decoding, not on the string length); every failure — missing field, bad scheme, empty envelope, oversize envelope, insert failure — returns 400, so assert the message; the write is not idempotent (repeat POSTs create additional application rows); tenant_id is optional and defaults to null, so no tenant scoping is enforced on write; the route relies on the gateway default-deny authGate for authentication.",
        "operationId": "post_api_hdk_watermark",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "watermark_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "variant_id": "{{var:variant_id}}",
                    "scheme": "visible",
                    "payload_envelope": "eyJ3IjoxfQ==",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — variant_id, scheme, payload_envelope are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variant_id": {
                    "type": "string"
                  },
                  "scheme": {
                    "type": "string"
                  },
                  "payload_envelope": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "variant_id": "{{var:variant_id}}",
                "scheme": "visible",
                "payload_envelope": "eyJ3IjoxfQ==",
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/hdk/watermark/{id}": {
      "get": {
        "tags": [
          "hdk-watermark"
        ],
        "summary": "Fetch one watermark application by id",
        "description": "Fetches a single watermark application from hdk_watermark.application by its application id and returns it as { success: true, data }. QA edge cases: an unknown id returns 404 \"not found\", and a malformed id returns the same 404, so status alone does not distinguish them; the lookup is not tenant-filtered, so any authenticated caller holding an id can read the record; the response carries the stored payload_envelope bytes, so tests should assert the round-trip of the exact bytes POSTed (remembering the base64-vs-utf8 sniffing performed on write).",
        "operationId": "get_api_hdk_watermark_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "watermark_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk-diagnostic/drain": {
      "post": {
        "tags": [
          "hdk-diagnostic"
        ],
        "summary": "Drain queue",
        "description": "Drains the persistent hdk-diagnostic outbox: claims up to `limit` rows from hdk_diagnostic.event where drained_at IS NULL (oldest received_at first, FOR UPDATE SKIP LOCKED), stamps drained_at = now(), and returns the claimed rows plus their count with 200. The body is optional - an absent body or absent `limit` defaults to 1000. Edge cases: draining an empty outbox returns 200 with events: [] and count: 0, never 404; the drain is destructive and NOT idempotent - an immediate second call returns the next batch rather than the same one, because the first call already stamped drained_at; concurrent callers never receive overlapping rows (SKIP LOCKED); a non-numeric or negative `limit` is passed straight into SQL LIMIT and fails at the database rather than in a validation branch; there is no tenant scoping on this route, so it drains the outbox across all devices and tenants.",
        "operationId": "post_api_hdk_diagnostic_drain",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "drain_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "limit": 50,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "FST_ERR_CTP_EMPTY_JSON_BODY — Body cannot be empty when content-type is set to application/json"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "limit": 50
              }
            }
          }
        }
      }
    },
    "/api/hdk-diagnostic/events": {
      "post": {
        "tags": [
          "hdk-diagnostic"
        ],
        "summary": "Capture event",
        "description": "Captures a single device diagnostic event into the persistent hdk_diagnostic.event outbox (device_uuid, category, JSONB payload, occurred_at) and returns 202 with the generated event_id; a downstream drain worker later ships it to long-term storage. device_uuid and category are required; payload defaults to {} and occurred_at defaults to now() when omitted. Edge cases: an empty-string device_uuid or category is rejected by the falsy missing-fields check; duplicate submissions are NOT deduplicated - each call inserts a new row with a fresh event_id, so client retries create duplicates; an unparseable occurred_at becomes an Invalid Date and fails at the INSERT rather than in validation; payload is stringified into jsonb so oversized payloads are bounded only by the Fastify body limit; there is no FK on device_uuid and no tenant scoping, so events for an unknown device are accepted.",
        "operationId": "post_api_hdk_diagnostic_events",
        "parameters": [],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "level": {
                        "type": "number"
                      },
                      "charging": {
                        "type": "boolean"
                      }
                    }
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "{{static:test-device-uuid-001}}",
                "category": "battery",
                "payload": {
                  "level": 0.42,
                  "charging": false
                },
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/hdk-idp/claims": {
      "post": {
        "tags": [
          "hdk-idp"
        ],
        "summary": "Register claim",
        "description": "Registers (upserts) a device identity claim binding a device_uuid to a person_id, optionally storing a biometric template envelope and/or PIN envelope (base64 blobs). Requires device_uuid and person_id; envelopes are optional (a claim with neither credential is accepted). Idempotent upsert on (device_uuid, person_id): a repeat call COALESCE-updates envelopes (a null envelope does NOT overwrite an existing one) and returns 201 — no duplicate/conflict error. Edge cases: empty-string vs missing fields, non-base64 envelopes, repeat registrations.",
        "operationId": "post_api_hdk_idp_claims",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "claim_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "device-hdk-idp-e2e-001",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "biometric_template_envelope": "dGVzdC1iaW9tZXRyaWMtdGVtcGxhdGU=",
                    "pin_envelope": "dGVzdC1waW4tZW52ZWxvcGU=",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "person_id": {
                    "type": "string"
                  },
                  "biometric_template_envelope": {
                    "type": "string"
                  },
                  "pin_envelope": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "device-hdk-idp-e2e-001",
                "person_id": "<id from scim>",
                "biometric_template_envelope": "dGVzdC1iaW9tZXRyaWMtdGVtcGxhdGU=",
                "pin_envelope": "dGVzdC1waW4tZW52ZWxvcGU="
              }
            }
          }
        }
      }
    },
    "/api/hdk-idp/devices/{device_uuid}/claims": {
      "get": {
        "tags": [
          "hdk-idp"
        ],
        "summary": "List claims",
        "description": "Lists every device identity claim registered for one device_uuid, returning claim_id, device_uuid, person_id, the biometric_template_envelope and pin_envelope blobs, last_used_at and created_at. A device may carry several claims, one per bound person. Edge cases: an unknown or never-registered device_uuid returns 200 with claims: [] and never 404; device_uuid is a TEXT column, so any string including a non-UUID is a legal lookup key that simply matches nothing (no cast error); the result set is unpaginated and unordered, so a device with many bound persons returns every row in whatever order Postgres yields; the query filters on device_uuid alone with no tenant or persona predicate, so the caller's JWT tenant does not narrow the results.",
        "operationId": "get_api_hdk_idp_devices_device_uuid_claims",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "claim_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk-idp/offline-auth/log": {
      "post": {
        "tags": [
          "hdk-idp"
        ],
        "summary": "Sync biometric offline-auth",
        "description": "Records an authentication a device performed while offline, writing it to hdk_idp.offline_auth_log on the next sync and touching last_used_at on the matching (device_uuid, person_id) device_claim. All four fields - device_uuid, person_id, method and occurred_at - are required, and method must be one of biometric, pin or passkey (also enforced by a CHECK constraint on the table). Edge cases: an unrecognised method has its own distinct 400 branch separate from the missing-fields branch; there is no FK and no uniqueness constraint, so an unknown device/person is accepted and replaying the same event inserts a duplicate log row (not idempotent); when no claim matches, the last_used_at UPDATE affects zero rows silently and the call still returns 201; person_id must be a UUID and occurred_at must be a parseable timestamp - both are handled at the database and fail there, not in a validation branch.",
        "operationId": "post_api_hdk_idp_offline_auth_log",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "log_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "method": "biometric",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "person_id": {
                    "type": "string"
                  },
                  "method": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "device_uuid": "<id from claims>",
                "person_id": "<id from claims>",
                "method": "biometric",
                "occurred_at": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/api/hdk-permissions/devices/{device_uuid}/latest": {
      "get": {
        "tags": [
          "hdk-permissions"
        ],
        "summary": "Get latest snapshot",
        "description": "Returns the most recent permission-surface snapshot captured for a device - the newest hdk_permissions.surface_snapshot row for that device_uuid by taken_at DESC - including snapshot_id, tenant_id, persona_id, the permission_set JSONB and taken_at. Edge cases: unlike the sibling claims list, a device with no snapshot yet returns 404 NotFound rather than an empty payload, so this must be exercised after a POST /api/hdk-permissions/snapshots for the same device_uuid; device_uuid is a TEXT column, so any non-matching string including a non-UUID yields a clean 404 rather than a cast error; only one row is ever returned no matter how many snapshots exist, and ties on taken_at are broken arbitrarily; the lookup is keyed solely on device_uuid - the row's tenant_id is returned but is never checked against the caller's JWT tenant.",
        "operationId": "get_api_hdk_permissions_devices_device_uuid_latest",
        "parameters": [
          {
            "name": "device_uuid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "device_uuid path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "latest_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk-permissions/snapshots": {
      "post": {
        "tags": [
          "hdk-permissions"
        ],
        "summary": "Capture surface",
        "description": "Captures the current permission-surface state of a device: the permission_set map (permission name to boolean or string, stored as JSONB) for a device_uuid within a tenant, optionally scoped to a persona_id, returning 201 with the stored snapshot including its generated snapshot_id and taken_at. device_uuid, tenant_id and permission_set are required; persona_id is optional and stored as NULL when omitted. Edge cases: there is no uniqueness constraint, so every call appends a new historical row - repeat posts are intentionally NOT idempotent and build the timeline that GET /devices/:device_uuid/latest reads; an empty permission_set object {} passes the presence check and is accepted, while a missing key is rejected; tenant_id and persona_id must be valid UUIDs and are enforced by the column types, not by handler validation; tenant_id comes from the body and is never cross-checked against the caller's JWT tenant.",
        "operationId": "post_api_hdk_permissions_snapshots",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "snapshot_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "test-device-uuid-001",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "{{var:persona_id}}",
                    "permission_set": {
                      "camera": true,
                      "location": "when-in-use"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "permission_set": {
                    "type": "object",
                    "properties": {
                      "camera": {
                        "type": "boolean"
                      },
                      "location": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "device_uuid": "test-device-uuid-001",
                "tenant_id": "<id from auth>",
                "persona_id": "{{var:persona_id}}",
                "permission_set": {
                  "camera": true,
                  "location": "when-in-use"
                }
              }
            }
          }
        }
      }
    },
    "/api/hdk-sync/conflicts/resolve": {
      "post": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Resolve CRDT note conflict",
        "description": "Resolves a two-sided conflict for an event_type by applying that type's registered policy (crdt / lww / merge / event-sourcing / human-review) to input_a and input_b, optionally linking the outcome to a batch_id and an audit_entry_id. Edge cases: the event_type MUST already be registered via PUT /api/hdk-sync/event-type-policies — an unregistered type returns 409 UnregisteredEventType, not 404; input_a and input_b must both be present objects (a falsy or missing side is a 400); the catch block maps ANY resolver throw to 409, so unrelated resolver failures also present as UnregisteredEventType; a 'human-review' policy does not resolve inline, it enqueues a human-review task; batch_id and audit_entry_id are optional linkage only.",
        "operationId": "post_api_hdk_sync_conflicts_resolve",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "event_type": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "input_a": {
                      "ops": [
                        {
                          "position": 0,
                          "char": "A",
                          "ts": 1,
                          "replica_id": "d1"
                        }
                      ]
                    },
                    "input_b": {
                      "ops": [
                        {
                          "position": 0,
                          "char": "B",
                          "ts": 2,
                          "replica_id": "d2"
                        }
                      ]
                    },
                    "resolve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "UnregisteredEventType — event_type <type> has no registered conflict_policy"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string"
                  },
                  "input_a": {
                    "type": "object",
                    "properties": {
                      "ops": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "position": {
                              "type": "integer"
                            },
                            "char": {
                              "type": "string"
                            },
                            "ts": {
                              "type": "integer"
                            },
                            "replica_id": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  },
                  "input_b": {
                    "type": "object",
                    "properties": {
                      "ops": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "position": {
                              "type": "integer"
                            },
                            "char": {
                              "type": "string"
                            },
                            "ts": {
                              "type": "integer"
                            },
                            "replica_id": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "event_type": "<id from hdk-sync>",
                "input_a": {
                  "ops": [
                    {
                      "position": 0,
                      "char": "A",
                      "ts": 1,
                      "replica_id": "d1"
                    }
                  ]
                },
                "input_b": {
                  "ops": [
                    {
                      "position": 0,
                      "char": "B",
                      "ts": 2,
                      "replica_id": "d2"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/hdk-sync/event-type-policies/{event_type}": {
      "get": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Get clinical note policy",
        "description": "Fetches the conflict policy registered for a single event_type given as a path segment. Edge cases: the lookup is an exact, case-sensitive match on the event_type string, so a differently-cased or escaped value that does not decode to the exact registered string returns 404; an unregistered event_type also returns 404 (the route never lazily creates a policy). Global registry — no tenant scoping.",
        "operationId": "get_api_hdk_sync_event_type_policies_event_type",
        "parameters": [
          {
            "name": "event_type",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "event_type path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "event_type_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk-sync/event-type-policies": {
      "get": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "List policies",
        "description": "Lists every registered event-type conflict policy. Takes no query parameters — there is no paging, filtering or tenant scoping, so the full registry is returned on each call and an empty registry yields data.policies = [] with 200, not 404. Read-only and safe to poll; clients use it to learn which event types may be replayed before starting a batch.",
        "operationId": "get_api_hdk_sync_event_type_policies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "event_type_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Register RGA-text policy for clinical notes",
        "description": "Registers (upserts) the conflict-resolution policy for a sync event_type: which of crdt | lww | merge | event-sourcing | human-review applies when two device replays disagree, plus an optional free-text strategy_detail and a retention_class. Idempotent — re-PUTting the same event_type overwrites the policy and returns 200 (never 201), so it is safe to replay. Edge cases: an unknown conflict_policy or retention_class is rejected 400 by an explicit whitelist in the route; an empty-string event_type is treated as missing; strategy_detail and retention_class are optional; the registry is global (not tenant-scoped), so a policy registered by one tenant's operator governs every tenant's replays for that event_type.",
        "operationId": "put_api_hdk_sync_event_type_policies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "event_type_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "event_type": "clinical.note.edit.v1",
                    "conflict_policy": "human-review",
                    "strategy_detail": "human-review:dual-control",
                    "retention_class": "regulated",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string"
                  },
                  "conflict_policy": {
                    "type": "string"
                  },
                  "strategy_detail": {
                    "type": "string"
                  },
                  "retention_class": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "event_type": "clinical.note.edit.v1",
                "conflict_policy": "human-review",
                "strategy_detail": "human-review:dual-control",
                "retention_class": "regulated"
              }
            }
          }
        }
      }
    },
    "/api/hdk-sync/human-review/open": {
      "get": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "List open review tasks",
        "description": "Lists the open human-review tasks produced when a conflict's policy is 'human-review'. Optional ?assignee_persona_id= narrows the queue to one reviewer; omitting it returns every open task. Edge cases: an empty queue is 200 with data.tasks = [], never 404; an assignee_persona_id matching nobody also yields an empty list rather than an error; there is no paging or limit parameter, so a large backlog is returned in full.",
        "operationId": "get_api_hdk_sync_human_review_open",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "open_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/hdk-sync/human-review/{task_id}/resolve": {
      "post": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Mark task resolved",
        "description": "Resolves a queued human-review task by setting its status (open | in-review | resolved | rejected) and recording the reviewer's chosen resolved_value. resolved_value is optional and defaults to NULL, so a task may be rejected with no payload. Edge cases: status is checked against an explicit whitelist, so a missing OR unrecognised status is a 400; an unknown :task_id returns 404; there is no state-machine guard — re-resolving an already-resolved task is accepted as long as the row still matches, so idempotency must be enforced by the caller.",
        "operationId": "post_api_hdk_sync_human_review_task_id_resolve",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "task_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "resolved",
                    "resolved_value": {
                      "decision": "merge"
                    },
                    "resolve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid status"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  },
                  "resolved_value": {
                    "type": "object",
                    "properties": {
                      "decision": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "status": "resolved",
                "resolved_value": {
                  "decision": "merge"
                }
              }
            }
          }
        }
      }
    },
    "/api/hdk-sync/replay/{batch_id}/complete": {
      "post": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Complete replay",
        "description": "Closes a replay batch, stamping it complete with the conflict_count the caller observed. The body is optional — an absent conflict_count defaults to 0. Edge cases: an unknown :batch_id returns 404; a non-UUID :batch_id is not validated by the route, reaches Postgres and surfaces as a 500 rather than a 400; there is no already-completed guard, so behaviour on a double-complete depends on whether the update still matches the row.",
        "operationId": "post_api_hdk_sync_replay_batch_id_complete",
        "parameters": [
          {
            "name": "batch_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "batch_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "complete_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "conflict_count": 0,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "conflict_count": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "conflict_count": 0
              }
            }
          }
        }
      }
    },
    "/api/hdk-sync/replay/start": {
      "post": {
        "tags": [
          "hdk-sync"
        ],
        "summary": "Start replay with one envelope",
        "description": "Opens a replay batch for one device: accepts the offline-queued envelopes a device accumulated and returns the created batch plus the subset that was rejected. Returns 201 even when EVERY envelope is rejected — inspect data.rejected rather than the status code. Edge cases: envelopes must be an array, but an EMPTY array is accepted and creates a zero-item batch; envelopes whose event_type has no registered policy come back in data.rejected instead of failing the request; batch size is not capped by the route, so payload size is bounded only by the gateway body limit; device_uuid and tenant_id come from the body, not the JWT, so tenant scoping is caller-asserted.",
        "operationId": "post_api_hdk_sync_replay_start",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "start_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "device_uuid": "test-device-uuid-001",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "envelopes": [],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "device_uuid": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "envelopes": {
                    "type": "array",
                    "items": {}
                  }
                }
              },
              "example": {
                "device_uuid": "test-device-uuid-001",
                "tenant_id": "<id from auth>",
                "envelopes": []
              }
            }
          }
        }
      }
    },
    "/api/identity/aliases": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Attach a phone alias to the registered person",
        "description": "Merges an external identifier (alias) onto a person record so that logins arriving through different channels resolve to the same identity, returning 201 with the stored alias. Edge cases: person_id, kind and value are all mandatory and a missing one yields a combined 400; kind is enum-checked against email, phone, gov_id, biometric_template_ref, social_idp_subject and saml_nameid, and any other value is rejected with the allowed list echoed in the message; a person_id that does not exist surfaces as 404 NotFound because the service error message contains \"not found\"; merging an alias that is already attached to the same person is treated as an idempotent merge rather than a duplicate error, but attaching a value already bound to a different person collides at the datastore and surfaces as a 500 rather than a 409; values are stored as supplied, so casing and formatting are not normalised at this layer.",
        "operationId": "post_api_identity_aliases",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "alias_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "phone",
                    "value": "+15555550123",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — person_id, kind, value are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <entity> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "kind": "phone",
                "value": "<generated phone>"
              }
            }
          }
        }
      }
    },
    "/.well-known/jwks.json": {
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "JWKS keys array",
        "description": "Publishes the JSON Web Key Set used to verify tokens issued by this gateway, served under /.well-known/ and therefore exempt from the default-deny auth gate — it is intentionally public and requires no bearer token. The response carries cache-control: public, max-age=300, so clients and proxies may serve a cached copy for up to five minutes; a key rotation is therefore not immediately visible to every verifier. Edge cases: the key set is derived from the JWT_SECRET environment variable and falls back to the literal \"change-me-in-prod\" when it is unset, so a misconfigured deployment still returns 200 with a key set built from the placeholder secret rather than failing — a QA check should assert the served key actually verifies a freshly minted token; the handler takes no parameters and has no failure branch of its own.",
        "operationId": "get_well_known_jwks_json",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "jwks.json_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": []
      }
    },
    "/.well-known/openid-configuration": {
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Discovery doc shape",
        "description": "Serves the OIDC discovery document describing this gateway as an identity provider — issuer, authorization/token/userinfo endpoints and jwks_uri. Served under /.well-known/ so it is on the gateway public allowlist and needs no bearer token. Edge cases: the issuer is built from the request itself — x-forwarded-proto (falling back to the connection protocol) plus the Host header — so a request arriving with a spoofed or missing Host produces a discovery document advertising the wrong issuer, and the handler falls back to the literal \"localhost:3000\" when Host is absent; behind nginx the proxy must set x-forwarded-proto or the advertised URLs come back as http on an https deployment; the response is not cached and the handler has no validation or not-found branch.",
        "operationId": "get_well_known_openid_configuration",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "openid_configuration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": []
      }
    },
    "/api/userinfo": {
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Userinfo for the freshly-registered subject (name/email/roles)",
        "description": "Returns the profile of the currently authenticated principal, merging the stored person record with the verified JWT claims, and is the endpoint the portals call to resolve a session cookie into a user. Edge cases: requires a bearer token, so a missing or expired token is 401 from requireAuth; a token that verifies but carries no sub claim is a second, distinct 401 raised inside the handler (\"Missing person_id claim\"); a token whose sub references a person row that has been deleted returns 404 NotFound even though the token is still cryptographically valid — clients must treat that as a forced re-login rather than a transient error; the response merges req.auth over the stored record, so claim values shadow stored fields of the same name; there are no parameters and the call is read-only and idempotent.",
        "operationId": "get_api_userinfo",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "userinfo_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Person not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/impersonation/{grant_id}/approve": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Approve with manager approval + customer consent refs",
        "description": "Records the dual-control approval on an impersonation grant by setting manager_approval_id and/or customer_consent_ref, returning the grant with its recomputed status. Requires a valid tenant JWT (requireAuth). Status is derived, not stored: it stays \"pending_approval\" until BOTH manager_approval_id and customer_consent_ref are non-null, so approving with only one of the two returns 200 with status still pending_approval - send both, or call twice, to reach \"active\". Edge cases: sending neither field is a 400; an unknown grant_id is a 404; both fields are cast to ::uuid so a non-UUID value is a 500 rather than a 400; there is no state guard, so an already-approved, already-ended or expired grant can be re-approved and simply has its columns overwritten (an expired grant recomputes to \"ended\", not \"active\"); the caller identity is never checked against the grant support_user_id, so self-approval is not prevented here.",
        "operationId": "post_api_impersonation_grant_id_approve",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "approve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "manager_approval_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "customer_consent_ref": "{{var:customer_consent_ref}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — At least one of manager_approval_id or customer_consent_ref must be provided"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Impersonation grant <grant_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "manager_approval_id": {
                    "type": "string"
                  },
                  "customer_consent_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "manager_approval_id": "<generated uuid>",
                "customer_consent_ref": "{{var:customer_consent_ref}}"
              }
            }
          }
        }
      }
    },
    "/api/impersonation/{grant_id}/end": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "End the active impersonation session",
        "description": "Terminates an impersonation grant: stamps a generated certificate_audit_id and pulls expires_at back to LEAST(expires_at, now()), returning the grant with status forced to \"ended\". Requires a valid tenant JWT (requireAuth). Edge cases: no body is read, so any payload is ignored; there is no state precondition - a grant still in pending_approval, one already ended, or one already expired can all be ended and return 200; it is repeatable but not strictly idempotent, because each call mints a fresh certificate_audit_id that overwrites the previous one; an unknown grant_id is a 404; a non-UUID grant_id fails the uuid comparison and is a 500; the caller identity is never compared to the grant support_user_id, so any authenticated caller can end any grant.",
        "operationId": "post_api_impersonation_grant_id_end",
        "parameters": [
          {
            "name": "grant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "grant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "end_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Impersonation grant <grant_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/impersonation/request": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Support engineer requests 30min impersonation",
        "description": "Opens a support impersonation grant against a target tenant, recording support_user_id, target_tenant_id and the ticket_ref, and returns 201 with the grant. Requires a valid tenant JWT (requireAuth). duration_minutes is optional and clamped server-side to the 5..240 range (default 30), so 1 becomes 5 and 10000 becomes 240 rather than erroring. The new grant is always created in status \"pending_approval\" because manager_approval_id and customer_consent_ref are still null - it does not grant access yet. Edge cases: support_user_id, target_tenant_id and ticket_ref are presence-checked only (no UUID or existence check), so a non-existent or malformed target_tenant_id passes validation and fails on the INSERT as a 500; the caller JWT identity is never compared to support_user_id, so a caller can open a grant naming someone else; there is no duplicate suppression - repeating the same ticket_ref creates another grant.",
        "operationId": "post_api_impersonation_request",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "support_user_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ticket_ref": "SUP-998",
                    "duration_minutes": 30,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — support_user_id, target_tenant_id, ticket_ref are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "support_user_id": {
                    "type": "string"
                  },
                  "target_tenant_id": {
                    "type": "string"
                  },
                  "ticket_ref": {
                    "type": "string"
                  },
                  "duration_minutes": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "support_user_id": "<id from auth>",
                "target_tenant_id": "<id from auth>",
                "ticket_ref": "SUP-998",
                "duration_minutes": 30
              }
            }
          }
        }
      }
    },
    "/api/incidents": {
      "get": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "List incidents for the signup tenant",
        "description": "List incidents for a tenant, most-recently-updated first. Tenant-scoped via the required tenant_id query param; optionally filtered by status, severity and/or owner_persona_id, with limit/offset paging (defaults 50/0).",
        "operationId": "get_api_incidents",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "incident_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Open a high-severity data-quality incident",
        "description": "Open an operational incident in status 'open'. tenant_id, incident_type and title are required; severity (low|medium|high|critical, default medium), description, affected_records, owner/reported_by personas, source, subject_ref, sla_due_at (SLA deadline for the breach scan) and metadata are optional. Emits incident.opened.v1. Advance via POST /api/incidents/:incident_id/transition.",
        "operationId": "post_api_incidents",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "incident_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "incident_type": "data-quality",
                    "title": "Bad import batch flagged",
                    "severity": "high",
                    "reported_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "affected_records": [
                      "batch-42"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, incident_type and title are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "incident_type": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string"
                  },
                  "reported_by_persona_id": {
                    "type": "string"
                  },
                  "affected_records": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "incident_type": "data-quality",
                "title": "Bad import batch flagged",
                "severity": "high",
                "reported_by_persona_id": "<id from auth>",
                "affected_records": [
                  "batch-42"
                ]
              }
            }
          }
        }
      }
    },
    "/api/incidents/{incident_id}/evidence": {
      "get": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Read the timeline for the incident that has evidence",
        "description": "Read an incident's evidence timeline in chronological order (oldest first, ordered by occurred_at then created_at), optionally filtered to a single kind via the kind query param. Each entry carries its sdk-audit receipt (audit_entry_id, audit_seq, audit_entry_hash) so a reader can verify the entry against the immutable hash chain. Tenant-scoped via the required tenant_id query param. Edge cases: 400 when tenant_id is absent; an unknown or other-tenant incident_id is NOT a 404 here — it simply yields an empty evidence array, since the timeline is a filtered read rather than a record fetch; limit defaults to 200 and offset to 0 for paging a long timeline; entries whose audit emit was unavailable carry null receipt columns.",
        "operationId": "get_api_incidents_incident_id_evidence",
        "parameters": [
          {
            "name": "incident_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "incident_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "evidence_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Record the detection evidence for the created incident",
        "description": "Append one entry to an incident's evidence timeline (detected / root_cause / recovery / verification / note). The entry is FIRST written to the sdk-audit hash-chained ledger and the receipt (audit_entry_id, audit_seq, audit_entry_hash) is stored on the row and returned, so the evidence is provably un-altered after recording. Evidence is APPEND-ONLY: a database trigger rejects every UPDATE and DELETE on the table, so there is deliberately no PATCH or DELETE counterpart to this endpoint, and the FK is ON DELETE RESTRICT so an incident that has evidence cannot be erased. Appending root_cause / recovery / verification also projects the body onto the matching incident summary column, and the first 'detected' entry stamps detected_at (COALESCE preserves an earlier detection time). Edge cases: 400 when tenant_id, kind or body is missing, or kind is outside the enum; 404 when the incident does not exist for that tenant (tenant-scoped, so another tenant's incident reads as not-found); occurred_at is optional and defaults to now(), letting back-dated evidence be recorded while created_at still records when it was filed; if the audit ledger is unavailable the entry is still recorded with null receipt columns rather than being lost.",
        "operationId": "post_api_incidents_incident_id_evidence",
        "parameters": [
          {
            "name": "incident_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "incident_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "evidence_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "detected",
                    "body": "Dedup monitor flagged 412 duplicate leads in the nightly ingest window.",
                    "evidence_ref": "s3://ops-logs/dedup-scan.json",
                    "recorded_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "metadata": {
                      "detector": "dedup-monitor",
                      "duplicate_count": 412
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, kind and body are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "404": {
            "description": "NotFound — [sdk-incident] incident <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "recorded_by_persona_id": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "detector": {
                        "type": "string"
                      },
                      "duplicate_count": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "kind": "detected",
                "body": "Dedup monitor flagged 412 duplicate leads in the nightly ingest window.",
                "evidence_ref": "s3://ops-logs/dedup-scan.json",
                "recorded_by_persona_id": "<id from auth>",
                "occurred_at": "<generated pastdatetime>",
                "metadata": {
                  "detector": "dedup-monitor",
                  "duplicate_count": 412
                }
              }
            }
          }
        }
      }
    },
    "/api/incidents/{incident_id}": {
      "get": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Get the created incident",
        "description": "Fetch a single incident by id, tenant-scoped via the required tenant_id query param. Returns the full record including root_cause/recovery/verification notes and lifecycle timestamps. 404 when the incident is not found for the tenant.",
        "operationId": "get_api_incidents_incident_id",
        "parameters": [
          {
            "name": "incident_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "incident_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "incident_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Assign an owner + record root cause",
        "description": "Update editable incident fields (title, description, severity, affected_records, root_cause, recovery, verification, owner_persona_id assignment, subject_ref, sla_due_at, metadata). Status is NOT editable here — use the transition endpoint. tenant_id is required. Emits incident.updated.v1. 404 if not found.",
        "operationId": "patch_api_incidents_incident_id",
        "parameters": [
          {
            "name": "incident_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "incident_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "incident_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "root_cause": "Upstream feed schema drift",
                    "severity": "critical",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  },
                  "root_cause": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "owner_persona_id": "<id from auth>",
                "root_cause": "Upstream feed schema drift",
                "severity": "critical"
              }
            }
          }
        }
      }
    },
    "/api/incidents/{incident_id}/transition": {
      "post": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Start investigating (open -> investigating)",
        "description": "Advance an incident's status. Valid transitions: open->investigating|mitigated, investigating->mitigated|resolved, mitigated->resolved|investigating, resolved->closed|investigating (regression re-open); an incident may be cancelled while pre-resolved. resolved stamps resolved_at, closed/cancelled stamp closed_at. Emits incident.transitioned.v1 (from/to). tenant_id and status required. 409 InvalidTransition when not allowed from the current state; 404 if not found.",
        "operationId": "post_api_incidents_incident_id_transition",
        "parameters": [
          {
            "name": "incident_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "incident_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "investigating",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and status are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — invalid transition <from> -> <to>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "status": "investigating"
              }
            }
          }
        }
      }
    },
    "/api/incidents/sla-breaches": {
      "get": {
        "tags": [
          "sdk-incident"
        ],
        "summary": "Scan SLA breaches for the signup tenant",
        "description": "SLA-breach scan: returns tenant incidents whose sla_due_at is past AND whose status is still active (not resolved/closed/cancelled), ordered by deadline ascending. Powered by the incident_sla_idx partial index. Tenant-scoped via the required tenant_id query param; optional limit (default 100). Returns an empty list when nothing is overdue.",
        "operationId": "get_api_incidents_sla_breaches",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "sla_breach_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/ingest/customer/batch": {
      "post": {
        "tags": [
          "sdk-ingest"
        ],
        "summary": "Bulk upsert customer records with idempotency key",
        "description": "sdk-ingest batch front door. Accepts an envelope { entity, mode: upsert|insert, idempotency_key, records[] } and imports records into the target entity. Records provenance via sdk-lineage and an append-only entry via sdk-audit. Returns per-record results { imported, skipped, errors[] }.",
        "operationId": "post_api_ingest_customer_batch",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "entity": "customer",
                    "mode": "upsert",
                    "idempotency_key": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "records": [
                      {
                        "external_id": "C-1001",
                        "name": "Acme Corp",
                        "email": "ops@acme.test"
                      },
                      {
                        "external_id": "C-1002",
                        "name": "Globex",
                        "email": "ap@globex.test"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — entity is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entity": {
                    "type": "string"
                  },
                  "mode": {
                    "type": "string"
                  },
                  "idempotency_key": {
                    "type": "string"
                  },
                  "records": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "external_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "entity": "customer",
                "mode": "upsert",
                "idempotency_key": "<generated uuid>",
                "records": [
                  {
                    "external_id": "C-1001",
                    "name": "Acme Corp",
                    "email": "ops@acme.test"
                  },
                  {
                    "external_id": "C-1002",
                    "name": "Globex",
                    "email": "ap@globex.test"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/ingest/{entity}/batch": {
      "post": {
        "tags": [
          "sdk-ingest"
        ],
        "summary": "Land a one-record batch for an entity (idempotent upsert)",
        "description": "Generic ETL landing endpoint: accepts a batch of records for the :entity named in the path, keyed by a caller-supplied idempotency_key, with an optional mode. Gated by the gateway default-deny authGate, so a valid tenant JWT is required; tenant_id is taken from req.auth.tenant (the JWT) and never from the body, so a batch cannot be written into another tenant. Edge cases: idempotency_key is mandatory and is what makes a replayed batch a no-op rather than a duplicate insert - re-POSTing the same key must not double-write; records must be a non-empty array, so an empty array and a missing records field both 400; the :entity segment is not checked against an allowlist in the route, so an unknown entity fails inside the service; there is no batch-size cap in the handler, so oversized batches are bounded only downstream; every service failure is caught and flattened to a 400 with the raw message, so this route never returns 404 or 500.",
        "operationId": "post_api_ingest_entity_batch",
        "parameters": [
          {
            "name": "entity",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "entity path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "mode": "upsert",
                    "idempotency_key": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "records": [
                      {
                        "external_id": "EXT-1001",
                        "name": "Demo Record",
                        "value": 42
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — entity is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string"
                  },
                  "idempotency_key": {
                    "type": "string"
                  },
                  "records": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "external_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "value": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "mode": "upsert",
                "idempotency_key": "<generated uuid>",
                "records": [
                  {
                    "external_id": "EXT-1001",
                    "name": "Demo Record",
                    "value": 42
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/ingest/sensor-readings/batch": {
      "post": {
        "tags": [
          "sdk-ingest"
        ],
        "summary": "Idempotent batch of catalog-valid sensor readings",
        "description": "Typed P12 time-series intake: writes a batch of sensor readings keyed by a caller-supplied idempotency_key and answers 201 with the ingest summary. Gated by the gateway default-deny authGate, so a valid tenant JWT is required; tenant_id prefers req.auth.tenant (the JWT) and only falls back to body.tenant_id when the JWT carries no tenant claim. Edge cases: idempotency_key is mandatory and is the replay guard - re-POSTing the same key must not double-write the series; readings must be a non-empty array, so both an empty array and an omitted field 400; if the time-series sink is not configured the route returns 400 \"sensor-reading sink not configured\" rather than a 503, which makes an infrastructure outage look like a client error; malformed reading shapes, bad timestamps and unknown sensor ids all surface as 400 with the raw service message; there is no batch-size cap in the handler; this route never returns 404 or 500 because every throw is flattened to 400.",
        "operationId": "post_api_ingest_sensor_readings_batch",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "idempotency_key": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "readings": [
                      {
                        "sensor_id": "{{var:sensor_id}}",
                        "asset_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                        "component_id": "{{var:component_id}}",
                        "ts": "2026-01-15T10:30:00Z",
                        "value": 12.5,
                        "unit": "N",
                        "quality": "good"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — idempotency_key is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "idempotency_key": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "readings": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "sensor_id": {
                          "type": "string"
                        },
                        "asset_id": {
                          "type": "string"
                        },
                        "component_id": {
                          "type": "string"
                        },
                        "ts": {
                          "type": "string"
                        },
                        "value": {
                          "type": "number"
                        },
                        "unit": {
                          "type": "string"
                        },
                        "quality": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "idempotency_key": "<generated uuid>",
                "tenant_id": "<id from auth>",
                "readings": [
                  {
                    "sensor_id": "{{var:sensor_id}}",
                    "asset_id": "<id from assets>",
                    "component_id": "{{var:component_id}}",
                    "ts": "<generated pastdatetime>",
                    "value": 12.5,
                    "unit": "N",
                    "quality": "good"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/intent/plan": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Plan a goal that matches a seeded capability edge (subject Patient)",
        "description": "Compiles a SemanticIntent into an executable Plan (G9 AC-8): resolves intent.subject.type to a semantic.object_type in the given ontology, selects matching semantic.capability_graph_edge rows by goal-keyword / pre-condition / post-condition fit, topologically orders them, persists an intent_plan row with status 'proposed', and emits a semantic.intent.planned.v1 audit entry. Returns 200 {success:true, data:{plan_id, intent_id, subject_id, steps[], generated_at, status}}. QA edge cases: five fields are mandatory and checked as one branch — tenant_id, ontology_id, goal, subject and trace_id; any missing one (or a missing `intent` wrapper) gives the same 400 'intent missing required fields'. Note subject.id is NOT validated, so an intent with subject:{type:'Patient'} and no id still plans and yields steps with an undefined subject argument. Everything after the presence check is caught and returned as 400, so semantic misses that a tester would expect as 404 are 400s instead: an unknown subject type in the ontology, and — the most common empty-result case — a goal whose tokens match no capability edge ('no capability_graph_edge matches goal ...'). A goal of one noise word therefore fails rather than returning an empty plan. Intent persistence is idempotent on (tenant_id, goal): re-posting the same tenant+goal reuses the existing intent_id but always mints a NEW plan_id, so the endpoint as a whole is not idempotent. agent_run_id is optional and defaults to null. There is no pagination — every matched step is inlined into steps[]. The path is not on the gateway public allowlist, so a valid tenant JWT is required; tenant_id comes from the body and is not cross-checked against the token.",
        "operationId": "post_intent_plan",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "intent": {
                      "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "ontology_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "goal": "summarize clinical note",
                      "subject": {
                        "type": "Patient",
                        "id": "{{var:subject_id}}"
                      },
                      "parameters": {},
                      "trace_id": "{{var:trace_id}}"
                    },
                    "agent_run_id": null,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — intent missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "intent": {
                    "type": "object",
                    "properties": {
                      "tenant_id": {
                        "type": "string"
                      },
                      "ontology_id": {
                        "type": "string"
                      },
                      "goal": {
                        "type": "string"
                      },
                      "subject": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string"
                          },
                          "id": {
                            "type": "string"
                          }
                        }
                      },
                      "parameters": {
                        "type": "object",
                        "properties": {}
                      },
                      "trace_id": {
                        "type": "string"
                      }
                    }
                  },
                  "agent_run_id": {
                    "nullable": true
                  }
                }
              },
              "example": {
                "intent": {
                  "tenant_id": "<id from auth>",
                  "ontology_id": "<id from ontology>",
                  "goal": "summarize clinical note",
                  "subject": {
                    "type": "Patient",
                    "id": "{{var:subject_id}}"
                  },
                  "parameters": {},
                  "trace_id": "{{var:trace_id}}"
                },
                "agent_run_id": null
              }
            }
          }
        }
      }
    },
    "/api/keys": {
      "get": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "List API keys for the tenant",
        "description": "Lists the API keys issued to the tenant named in the required tenant_id query param, delegating to sdk-api-keys so the canonical schema (prefix, key_hash BYTEA, scopes[], synthetic_persona_id) is honoured. Never returns key plaintext - only metadata. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: tenant_id comes from the query string rather than the JWT, so a caller may list another tenant keys - tenant scoping must be tested explicitly, and an absent tenant_id is a 400 rather than an implicit self-scope; a tenant with no keys returns 200 with an empty array, not a 404; revoked keys are included in the result, so the caller must filter on state; there is no paging, so a tenant with many keys returns the whole set; a non-UUID tenant_id fails the uuid cast inside listKeys and surfaces as a 500 echoing the raw driver message.",
        "operationId": "get_api_keys",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "ListFailed — <error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Issue a new API key for the tenant",
        "description": "Issues a new API key for a tenant and returns {key_id, plaintext}. The plaintext is returned exactly once and is never recoverable afterwards (only the hash is stored), so the caller MUST capture it from this response. Scopes may be supplied either as scopes[] or as a single scope string, which is normalised into a one-element array. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: tenant_id is read from the body, not the JWT, so a caller may mint a key for another tenant - verify tenant scoping deliberately; an absent tenant_id and an empty effective scope set (no scopes[], no scope, or scopes:[]) produce the same single 400; scope strings are not validated against an allowlist in this route, so an unknown scope reaches issueKey; the endpoint is not idempotent - repeating the identical request mints an additional distinct key rather than returning the existing one; a non-UUID tenant_id or an unsatisfied tenant foreign key surfaces as a 500 with the raw driver message rather than a 400 or 404.",
        "operationId": "post_api_keys",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "production-integration-key",
                    "scope": "crm.contact.read",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id + scope(s) required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "IssueFailed — <error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "production-integration-key",
                "scope": "crm.contact.read"
              }
            }
          }
        }
      }
    },
    "/api/keys/{key_id}/revoke": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Revoke the freshly-issued key",
        "description": "Revokes the API key named by the key_id path param, requiring a non-empty reason in the body, and returns {success:true} on the first successful revocation. Gated by the gateway default-deny authGate, so a valid tenant JWT is required. Edge cases: reason is trimmed before the check, so a whitespace-only string counts as missing and is a 400 - but note the reason is never persisted by this handler, it only gates the call; revocation is NOT idempotent in its response - a second revoke of the same key returns 404 \"key not found or already revoked\", the same status and message as a key that never existed, so the two cases are indistinguishable; there is no tenant check at all - the key is matched on key_id alone, so a caller can revoke another tenant key; a non-UUID key_id fails the uuid cast inside revokeKey and surfaces as a 500 with the raw driver message.",
        "operationId": "post_api_keys_key_id_revoke",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "compromised-credential-rotation",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — reason required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — key not found or already revoked"
          },
          "500": {
            "description": "RevokeFailed — <error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "compromised-credential-rotation"
              }
            }
          }
        }
      }
    },
    "/api/lead-scoring/models/active": {
      "get": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Look up the active model for (tenant, vertical)",
        "description": "Resolves the currently active model for a (tenant_id, vertical) pair, ordered by trained_at DESC NULLS LAST and limited to one row. Both query params are required. QA edge cases: a tenant/vertical pair with no active model returns 404 'no active model' — distinct from the 400 you get when the params are simply absent; because model creation does not enforce a single active model per pair, several rows can be active at once and this endpoint deterministically returns only the most recently trained one; tenant_id is cast to ::uuid, so a non-UUID value raises a database error rather than a clean 400; vertical matching is exact and case-sensitive.",
        "operationId": "get_api_lead_scoring_models_active",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "active_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and vertical query params required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — no active model"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/lead-scoring/models/{id}/activate": {
      "post": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Activate a model (flip status to active)",
        "description": "Flips a lead-scoring model to status='active' and stamps trained_at = now(), returning the updated model. QA edge cases: the UPDATE has no state precondition — activating an already-active model succeeds and simply re-stamps trained_at (idempotent in status, NOT in trained_at, which moves on every call, and trained_at is the tiebreaker used by /models/active); a retired model can be re-activated with no guard; activating does not deactivate any sibling model for the same (tenant, vertical), so this endpoint can leave multiple active models; an unknown model_id yields no updated row and the thrown error is mapped to 404.",
        "operationId": "post_api_lead_scoring_models_id_activate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "activate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — [sdk-lead-scoring] model <id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/lead-scoring/models/{id}": {
      "get": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Fetch a model by id",
        "description": "Fetches one lead-scoring model by model_id, returning model_id, tenant_id, vertical, trained_at (ISO or null), feature_set and status. QA edge cases: an unknown model_id returns 404 'not found'; the query is not tenant-scoped, so any caller with a model_id reads that tenant's model metadata; trained_at is null for models still in 'training' status, so consumers must handle the null; the response contains the model row only — feature weights require the separate /weights endpoint.",
        "operationId": "get_api_lead_scoring_models_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "model_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/lead-scoring/models/{id}/retire": {
      "post": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Retire a model (flip status to retired)",
        "description": "Flips a lead-scoring model to status='retired', returning the updated model. Unlike activate, trained_at is left untouched. QA edge cases: there is no state precondition — retiring an already-retired model succeeds and is fully idempotent; retiring the only active model for a (tenant, vertical) is permitted and leaves that pair with no active model, which then makes /models/active return 404 and every /score call for that pair fail with 400 'no active model' — that downstream blast radius is the important regression to cover; an unknown model_id maps to 404.",
        "operationId": "post_api_lead_scoring_models_id_retire",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "retire_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — [sdk-lead-scoring] model <id> not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/lead-scoring/models/{id}/weights/{feature}": {
      "put": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Tune one feature weight (upsert)",
        "description": "Tunes a single feature weight for a model. Implemented as an INSERT ... ON CONFLICT (model_id, feature) DO UPDATE, so it upserts: the first call creates the weight row, later calls overwrite the value and re-stamp last_tuned_at. QA edge cases: the feature name comes from the URL path and is NOT validated against DEFAULT_FEATURE_WEIGHTS, so tuning an arbitrary feature like 'made_up_signal' succeeds and adds a row that scoreContact will include in its normalisation total but never multiply by a subscore — silently diluting every real subscore, which is the highest-value bug to test for; weight must be a finite number >= 0, so 0 is accepted (and setting every weight to 0 makes normalisation skip and produce a composite of 0) while negative, NaN, Infinity, null and non-numeric values are rejected; an unknown model_id violates the feature_weight foreign key and surfaces as 400, not 404.",
        "operationId": "put_api_lead_scoring_models_id_weights_feature",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          },
          {
            "name": "feature",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "feature path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "weight_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "weight": 0.4,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — weight must be a non-negative finite number"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "weight": {
                    "type": "number"
                  }
                }
              },
              "example": {
                "weight": 0.4
              }
            }
          }
        }
      }
    },
    "/api/lead-scoring/models/{id}/weights": {
      "get": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "List feature weights for a model",
        "description": "Lists every lead_scoring.feature_weight row for a model, ordered by feature name, with weight coerced from the database numeric to a JS number. QA edge cases: an unknown model_id does NOT 404 — it returns 200 with an empty array, so \"model does not exist\" and \"model has no weights\" are indistinguishable through this endpoint (use GET /models/:id to disambiguate); an empty array is meaningful, because the scoring engine falls back to DEFAULT_FEATURE_WEIGHTS whenever no weight rows exist; weights are returned raw (as tuned), NOT normalised — normalisation to sum 1.0 happens inside scoreContact, so these values will not necessarily add up to 1; the listing is unpaginated and not tenant-scoped.",
        "operationId": "get_api_lead_scoring_models_id_weights",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "weight_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/lead-scoring/models": {
      "post": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Create an active lead-scoring model for (tenant, vertical)",
        "description": "Creates a lead-scoring model for a (tenant_id, vertical) pair and, in the same transaction, seeds one lead_scoring.feature_weight row per entry in weights — defaulting to DEFAULT_FEATURE_WEIGHTS (proximity 0.30, expertise 0.25, intent 0.30, storm_impact 0.15) when weights is omitted. Status is 'active' with trained_at = now() when activate is true, otherwise 'training' with trained_at null. QA edge cases: creation is NOT idempotent and nothing enforces one model per (tenant, vertical), so repeated POSTs with activate:true leave several active models and the /models/active lookup then resolves the most recently trained one; tenant_id is cast to ::uuid in SQL, so a non-UUID tenant_id fails inside the transaction and surfaces as 400 rather than a distinct validation error; passing an empty weights object ({}) is accepted and creates a model with zero weight rows, which later makes scoring fall back to DEFAULT_FEATURE_WEIGHTS; vertical is a free-text string with no allowlist.",
        "operationId": "post_api_lead_scoring_models",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "model_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "vertical": "solar",
                    "feature_set": {
                      "proximity": {},
                      "expertise": {},
                      "intent": {},
                      "storm_impact": {}
                    },
                    "weights": {
                      "proximity": 0.3,
                      "expertise": 0.25,
                      "intent": 0.3,
                      "storm_impact": 0.15
                    },
                    "activate": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and vertical are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "vertical": {
                    "type": "string"
                  },
                  "feature_set": {
                    "type": "object",
                    "properties": {
                      "proximity": {
                        "type": "object",
                        "properties": {}
                      },
                      "expertise": {
                        "type": "object",
                        "properties": {}
                      },
                      "intent": {
                        "type": "object",
                        "properties": {}
                      },
                      "storm_impact": {
                        "type": "object",
                        "properties": {}
                      }
                    }
                  },
                  "weights": {
                    "type": "object",
                    "properties": {
                      "proximity": {
                        "type": "number"
                      },
                      "expertise": {
                        "type": "number"
                      },
                      "intent": {
                        "type": "number"
                      },
                      "storm_impact": {
                        "type": "number"
                      }
                    }
                  },
                  "activate": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "vertical": "solar",
                "feature_set": {
                  "proximity": {},
                  "expertise": {},
                  "intent": {},
                  "storm_impact": {}
                },
                "weights": {
                  "proximity": 0.3,
                  "expertise": 0.25,
                  "intent": 0.3,
                  "storm_impact": 0.15
                },
                "activate": true
              }
            }
          }
        }
      }
    },
    "/api/lead-scoring/next-best-action": {
      "post": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Score + recommend next best action against the active model",
        "description": "Scores a contact and recommends the next action in one round trip: it runs the same scoreContact pipeline as /score (so it also persists a lead_scoring.score row and emits the audit event) and then maps the result to an action. The default resolver applies storm_impact >= 0.5 as an override returning 'storm_response' regardless of any higher subscore; otherwise the dominant subscore maps proximity -> schedule_visit, expertise -> send_offer, intent -> reach_out, and anything else to nurture; when every subscore is <= 0 it returns 'nurture'. QA edge cases: the storm override outranking a strictly higher proximity/intent score is the key branch to cover, as is the 0.5 boundary itself (0.49 vs 0.50); this endpoint has the same side effects as /score, so calling it twice writes two score rows — it is not a read-only preview; every failure mode of /score applies identically here (missing required fields and 'no active model' both surface as 400); the resolver is swappable at runtime via setNextBestActionResolver, so in an environment where a custom resolver is installed the action mapping above no longer holds.",
        "operationId": "post_api_lead_scoring_next_best_action",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "next_best_action_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "vertical": "solar",
                    "contact_id": "{{var:contact_id}}",
                    "trace_id": "{{var:trace_id}}",
                    "proximity": {
                      "distance_km": 8
                    },
                    "expertise": {
                      "persona_kinds": [
                        "homeowner",
                        "insurance-claimant"
                      ],
                      "vertical_specialties": [
                        "roof",
                        "flood"
                      ]
                    },
                    "intent": {
                      "days_since_last_engagement": 1,
                      "emails_opened": 6,
                      "replies": 2
                    },
                    "storm_impact": {
                      "overlapping_storm_events": 3
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, vertical, contact_id, trace_id are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "vertical": {
                    "type": "string"
                  },
                  "contact_id": {
                    "type": "string"
                  },
                  "trace_id": {
                    "type": "string"
                  },
                  "proximity": {
                    "type": "object",
                    "properties": {
                      "distance_km": {
                        "type": "integer"
                      }
                    }
                  },
                  "expertise": {
                    "type": "object",
                    "properties": {
                      "persona_kinds": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "vertical_specialties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "intent": {
                    "type": "object",
                    "properties": {
                      "days_since_last_engagement": {
                        "type": "integer"
                      },
                      "emails_opened": {
                        "type": "integer"
                      },
                      "replies": {
                        "type": "integer"
                      }
                    }
                  },
                  "storm_impact": {
                    "type": "object",
                    "properties": {
                      "overlapping_storm_events": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from lead-scoring>",
                "vertical": "solar",
                "contact_id": "{{var:contact_id}}",
                "trace_id": "{{var:trace_id}}",
                "proximity": {
                  "distance_km": 8
                },
                "expertise": {
                  "persona_kinds": [
                    "homeowner",
                    "insurance-claimant"
                  ],
                  "vertical_specialties": [
                    "roof",
                    "flood"
                  ]
                },
                "intent": {
                  "days_since_last_engagement": 1,
                  "emails_opened": 6,
                  "replies": 2
                },
                "storm_impact": {
                  "overlapping_storm_events": 3
                }
              }
            }
          }
        }
      }
    },
    "/api/lead-scoring/score": {
      "post": {
        "tags": [
          "sdk-lead-scoring"
        ],
        "summary": "Score a contact against the active model (happy path)",
        "description": "Scores one contact: resolves the active model for (tenant_id, vertical), loads and normalises its feature weights, runs the four subscore backends (proximity, expertise, intent, storm_impact) in parallel, computes the weighted composite scaled to 0-100, persists a lead_scoring.score row with the component breakdown, and emits a lead-scoring.scored.v1 audit entry. QA edge cases: the single most common failure is a 400 carrying 'no active model for tenant ... vertical ...' — every one of tenant_id, vertical, contact_id and trace_id is required, so a missing-field 400 and a no-model 400 share a status and must be distinguished by message; the audit append is wrapped in try/catch and only warns, so an audit outage still returns 200 (do not assert that scoring fails when audit is down); each of proximity/expertise/intent/storm_impact is optional and defaults to {}, which yields a 0 subscore rather than an error, so a body with only the four required ids scores 0 successfully; the call is not idempotent — the same trace_id can be posted repeatedly and produces a new score row each time; if the model has no weight rows the engine silently falls back to DEFAULT_FEATURE_WEIGHTS.",
        "operationId": "post_api_lead_scoring_score",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "score_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "vertical": "solar",
                    "contact_id": "{{var:contact_id}}",
                    "trace_id": "{{var:trace_id}}",
                    "proximity": {
                      "distance_km": 12
                    },
                    "expertise": {
                      "persona_kinds": [
                        "homeowner"
                      ],
                      "vertical_specialties": [
                        "roof",
                        "flood"
                      ]
                    },
                    "intent": {
                      "days_since_last_engagement": 3,
                      "emails_opened": 4,
                      "replies": 1
                    },
                    "storm_impact": {
                      "overlapping_storm_events": 2
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, vertical, contact_id, trace_id are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "vertical": {
                    "type": "string"
                  },
                  "contact_id": {
                    "type": "string"
                  },
                  "trace_id": {
                    "type": "string"
                  },
                  "proximity": {
                    "type": "object",
                    "properties": {
                      "distance_km": {
                        "type": "integer"
                      }
                    }
                  },
                  "expertise": {
                    "type": "object",
                    "properties": {
                      "persona_kinds": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "vertical_specialties": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "intent": {
                    "type": "object",
                    "properties": {
                      "days_since_last_engagement": {
                        "type": "integer"
                      },
                      "emails_opened": {
                        "type": "integer"
                      },
                      "replies": {
                        "type": "integer"
                      }
                    }
                  },
                  "storm_impact": {
                    "type": "object",
                    "properties": {
                      "overlapping_storm_events": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from lead-scoring>",
                "vertical": "solar",
                "contact_id": "{{var:contact_id}}",
                "trace_id": "{{var:trace_id}}",
                "proximity": {
                  "distance_km": 12
                },
                "expertise": {
                  "persona_kinds": [
                    "homeowner"
                  ],
                  "vertical_specialties": [
                    "roof",
                    "flood"
                  ]
                },
                "intent": {
                  "days_since_last_engagement": 3,
                  "emails_opened": 4,
                  "replies": 1
                },
                "storm_impact": {
                  "overlapping_storm_events": 2
                }
              }
            }
          }
        }
      }
    },
    "/api/mcp/health": {
      "get": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "Health probe returns sdk marker",
        "description": "Liveness probe for sdk-mcp-bridge; returns 200 with { sdk: \"sdk-mcp-bridge\", status: \"ok\" }. It is a constant-response handler with no database, no body parsing and no branches, so it cannot fail with a 4xx/5xx. The path ends in /health, so the api-gateway default-deny authGate treats it as public - no Authorization header is required and sending an invalid one does not cause a 401. There are therefore no error cases.",
        "operationId": "get_api_mcp_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/mcp/server-registrations": {
      "get": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "List MCP server registrations for a tenant",
        "description": "Lists the MCP server registrations for one tenant. The tenant_id query parameter is mandatory and is the only scoping mechanism - the handler does NOT derive the tenant from the JWT, so any authenticated caller can list another tenant's registrations by passing that tenant_id. Edge cases: tenant_id omitted or an empty string is a 400; an unknown tenant_id returns 200 with an empty array; disabled registrations are included in the listing; the listing is unpaginated; requires a valid JWT.",
        "operationId": "get_api_mcp_server_registrations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "server_registration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "Missing query param: tenant_id — Missing query param: tenant_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "List failed — List failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "Register a synthetic Slack MCP server",
        "description": "Register an external MCP server. Opens transport, calls tools/list to auto-register tools, emits mcp.server.registered.v1. Probe failure rolls back to status='disabled'. FR-MCP-1/2/7.",
        "operationId": "post_api_mcp_server_registrations",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "server_registration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "display_name": "Slack MCP",
                    "transport": "http",
                    "endpoint_url": "http://localhost:9401/mcp",
                    "credential_envelope_b64": "eyJ0b2tlbiI6InRlc3QtY3JlZCJ9",
                    "allowed_agent_ids": [],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Required: tenant_id, display_name, transport, endpoint_url, credential_envelope_b64 — Required: tenant_id, display_name, transport, endpoint_url, credential_envelope_b64"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Register failed — Register failed"
          },
          "502": {
            "description": "<probe failure message> — MCP tool-discovery probe failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "transport": {
                    "type": "string"
                  },
                  "endpoint_url": {
                    "type": "string"
                  },
                  "credential_envelope_b64": {
                    "type": "string"
                  },
                  "allowed_agent_ids": {
                    "type": "array",
                    "items": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "display_name": "Slack MCP",
                "transport": "http",
                "endpoint_url": "http://localhost:9401/mcp",
                "credential_envelope_b64": "{{static:eyJ0b2tlbiI6InRlc3QtY3JlZCJ9}}",
                "allowed_agent_ids": []
              }
            }
          }
        }
      }
    },
    "/api/mcp/server-registrations/{id}/disable": {
      "post": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "Disable a registered MCP server",
        "description": "Disables an MCP server registration, recording the acting subject and the supplied reason on the audit trail; returns 200 with { registration_id, disabled: true }. reason is mandatory. The actor defaults to the literal \"system\" when the JWT carries no sub claim. Edge cases: reason missing or an empty string is a 400; an unknown registration id is a 404 (the service throws a \"not found\" error which the handler maps); disabling an already-disabled registration succeeds again (idempotent - no conflict check); the handler does not verify the registration belongs to the caller's tenant; a non-UUID id fails the Postgres UUID cast and is reported as a generic 500; requires a valid JWT.",
        "operationId": "post_api_mcp_server_registrations_id_disable",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "disable_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "Decommissioned by automated test",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "reason is required — reason is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "<service \"not found\" message> — server_registration not found"
          },
          "500": {
            "description": "Disable failed — Disable failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "Decommissioned by automated test"
              }
            }
          }
        }
      }
    },
    "/api/mcp/server-registrations/{id}": {
      "get": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "Read a registered MCP server by id",
        "description": "Reads one MCP server registration by its registration id and returns 200 with the row. Read-only; the handler does not compare the row's tenant_id against the JWT, so any authenticated caller can read any registration id. Edge cases: an unknown but well-formed UUID returns 404; a non-UUID id fails the Postgres UUID cast (22P02) inside the try block and is reported as a generic 500 \"Lookup failed\" rather than a 400; a registration that has been disabled is still returned (with its disabled state) rather than 404ing; requires a valid JWT.",
        "operationId": "get_api_mcp_server_registrations_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "server_registration_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "server_registration not found — server_registration not found"
          },
          "500": {
            "description": "Lookup failed — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/mcp/tools/{tool_id}/invoke": {
      "post": {
        "tags": [
          "sdk-mcp-bridge"
        ],
        "summary": "Invoke an MCP tool via capability token",
        "description": "Capability-token-gated MCP tool invocation. Validates token against args, marks single-use, opens transport, calls /tools/call, persists mcp.tool_invocation + mcp.tool.invoked.v1. AC-12, FR-MCP-3.",
        "operationId": "post_api_mcp_tools_tool_id_invoke",
        "parameters": [
          {
            "name": "tool_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tool_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "invoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "agent_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capability_token_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "args": {
                      "channel": "#general",
                      "text": "hello from automated test"
                    },
                    "trace_id": "{{var:trace_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "tool_id must be a valid UUID — tool_id must be a valid UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "<invocation result payload> — Invocation outcome was not \"succeeded\""
          },
          "404": {
            "description": "<service \"not found\" message> — tool not found"
          },
          "500": {
            "description": "Invoke failed — Invoke failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_run_id": {
                    "type": "string"
                  },
                  "capability_token_id": {
                    "type": "string"
                  },
                  "args": {
                    "type": "object",
                    "properties": {
                      "channel": {
                        "type": "string"
                      },
                      "text": {
                        "type": "string"
                      }
                    }
                  },
                  "trace_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "agent_run_id": "<id from agent-runtime-runs>",
                "capability_token_id": "<id from agent-runtime-tokens>",
                "args": {
                  "channel": "#general",
                  "text": "hello from automated test"
                },
                "trace_id": "{{var:trace_id}}"
              }
            }
          }
        }
      }
    },
    "/api/me/profile": {
      "put": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Update name/phone/avatar for the signed-up founder",
        "description": "Updates the CURRENT caller's profile (person_id comes from the verified JWT sub, never from the body): display_name/given_name/family_name/avatar are merged into the profile.band_l2 'profile' band via a jsonb || upsert, while phone is stored as a person-level identity.alias. Returns 200 with { person_id, band_written, phone_updated }. Edge cases: the name/avatar band write needs a tenant context, resolved from the caller's OLDEST active identity.tenant_membership — a caller with no active membership still gets 200 but with band_written=false and the name fields silently dropped; only truthy fields are written, so empty strings are ignored and no field can be cleared this way; phone is replace-semantics (any existing phone alias is DELETEd first) and passing phone:\"\" clears it while still reporting phone_updated=true; the new phone alias is inserted with verified_at NULL and its hash needs pgcrypto digest(); the band upsert merges rather than replaces, so omitted keys are preserved. Requires a valid tenant JWT.",
        "operationId": "put_api_me_profile",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "profile_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "display_name": "Acme QA Sample",
                    "given_name": "Ada",
                    "family_name": "Lovelace",
                    "phone": "+15555550123",
                    "avatar": "https://cdn.example.com/avatar.png",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <Postgres error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "display_name": {
                    "type": "string"
                  },
                  "given_name": {
                    "type": "string"
                  },
                  "family_name": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "avatar": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "display_name": "<generated name>",
                "given_name": "Ada",
                "family_name": "Lovelace",
                "phone": "<generated phone>",
                "avatar": "https://cdn.example.com/avatar.png"
              }
            }
          }
        }
      }
    },
    "/api/media/{blob_id}/ready": {
      "post": {
        "tags": [
          "sdk-media"
        ],
        "summary": "Commit an uploaded blob with its SHA-256 checksum",
        "description": "Commits an upload: the client reports the SHA-256 it computed and the blob transitions to ready. Returns 200 with the updated blob. checksum_hex must be exactly 64 hex characters (an optional 0x prefix is stripped before the check). The blob's tenant is asserted against the JWT tenant_id claim, so committing another tenant's blob is a 403. Edge cases: a malformed or truncated checksum is a 400; an unknown blob_id is a 404 BlobNotFound; a blob whose bytes have already been cryptographically shredded is a 410 Gone; a blob attached to a sealed encounter is a 409; a JWT with no tenant_id claim is a 403 rather than a 401; re-posting ready for an already-ready blob is not specially guarded and simply repeats the state write.",
        "operationId": "post_api_media_blob_id_ready",
        "parameters": [
          {
            "name": "blob_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "blob_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ready_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "checksum_hex": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "BlobNotFound — Blob <blob_id> not found"
          },
          "409": {
            "description": "SealedEncounter — Encounter <encounter_id> is sealed; new evidence blocked per FR-MED-5"
          },
          "410": {
            "description": "Gone — Blob <blob_id> has been cryptographically shredded"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "checksum_hex": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "checksum_hex": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
              }
            }
          }
        }
      }
    },
    "/api/media/{blob_id}/playback-url": {
      "get": {
        "tags": [
          "sdk-media"
        ],
        "summary": "Issue 10min playback URL for the uploaded blob",
        "description": "Issues a presigned, time-limited playback/download URL for a ready blob (FR-MED-4) and returns 200 with the URL and expiry. Both a tenant_id claim and a sub claim are required on the JWT - a missing tenant_id is a 403 while a missing sub is a 401 - and the blob's tenant is asserted against the caller's. Edge cases: the optional ttl_seconds query parameter must parse as a positive finite number (0, negative and non-numeric values are 400s) and any oversized value is silently clamped down to the service maximum rather than rejected; an unknown blob_id is a 404; a blob owned by another tenant is a 403; a blob whose key material has been shredded is a 410 Gone; the access is attributed to the caller's persona, so this route is not side-effect free.",
        "operationId": "get_api_media_blob_id_playback_url",
        "parameters": [
          {
            "name": "blob_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "blob_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "playback_url_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — ttl_seconds must be a positive number"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "BlobNotFound — Blob <blob_id> not found"
          },
          "410": {
            "description": "Gone — Blob <blob_id> has been cryptographically shredded"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/media/{blob_id}/transcode": {
      "post": {
        "tags": [
          "sdk-media"
        ],
        "summary": "Enqueue image-optimize pipeline",
        "description": "Enqueues a transcode job for an existing blob (FR-MED-3) and returns 201 with the job record, whose status is then polled via GET /api/media/transcode-jobs/:job_id. pipeline is the only body field and must be one of video-mp4-hls | image-optimize | pdf-thumbnail. The blob's tenant is asserted against the JWT tenant_id claim. Edge cases: a missing or unrecognised pipeline is a 400 with the valid values echoed back; an unknown blob_id is a 404; a blob owned by another tenant is a 403; a shredded blob is a 410 Gone and cannot be transcoded; there is no duplicate-job guard, so requesting the same pipeline twice enqueues a second independent job rather than returning the first; a JWT with no tenant_id claim is a 403 rather than a 401.",
        "operationId": "post_api_media_blob_id_transcode",
        "parameters": [
          {
            "name": "blob_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "blob_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transcode_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "pipeline": "image-optimize",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "BlobNotFound — Blob <blob_id> not found"
          },
          "410": {
            "description": "Gone — Blob <blob_id> has been shredded; cannot transcode"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pipeline": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "pipeline": "image-optimize"
              }
            }
          }
        }
      }
    },
    "/api/media/transcode-jobs/{job_id}": {
      "get": {
        "tags": [
          "sdk-media"
        ],
        "summary": "Poll a transcode job by id",
        "description": "Polls the status of a transcode job by job_id and returns 200 with the job record (pipeline, state, output refs, timings). The route is behind requireAuth, but unlike the other media handlers this one does NOT call the tenant guard - it neither requires a tenant_id claim nor checks that the job belongs to the caller's tenant, so any authenticated caller can poll any job_id. Edge cases: an unknown job_id is a 404; a job that is still queued or running returns 200 with its in-progress state rather than an error, so callers must poll; a failed job likewise returns 200 with a failed state; a non-UUID job_id fails the Postgres UUID cast and, matching none of the mapped error classes, surfaces as a generic 500 InternalError.",
        "operationId": "get_api_media_transcode_jobs_job_id",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "job_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transcode_job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No transcode job <job_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/media/upload-url": {
      "post": {
        "tags": [
          "sdk-media"
        ],
        "summary": "Issue upload URL for a 1MB JPEG",
        "description": "Issues a presigned S3 upload URL for a new media blob (FR-MED-1) and returns 201 with the blob id, URL and expiry. The tenant is forced from the JWT tenant_id claim and overwrites any tenant_id in the body, so cross-tenant uploads are impossible. persona_id, content_type and a positive byte_size are mandatory. Edge cases: byte_size must be a positive number and is capped at 5 GiB (5368709120) - zero, negative, non-numeric or oversized values are 400s; ttl_seconds is optional and is clamped down to the service maximum, so an oversized TTL is silently reduced rather than rejected; a JWT with no tenant_id claim is a 403, not a 401; supplying an encounter_id that has already been sealed is a 409 (FR-MED-5); a tenant with no active vault key is a 400 VaultKeyMissing and must be provisioned via sdk-vault first.",
        "operationId": "post_api_media_upload_url",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "upload_url_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "content_type": "image/jpeg",
                    "byte_size": 1048576,
                    "ttl_seconds": 900,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "409": {
            "description": "SealedEncounter — Encounter <encounter_id> is sealed; new evidence blocked per FR-MED-5"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "content_type": {
                    "type": "string"
                  },
                  "byte_size": {
                    "type": "integer"
                  },
                  "ttl_seconds": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from auth>",
                "content_type": "image/jpeg",
                "byte_size": 1048576,
                "ttl_seconds": 900
              }
            }
          }
        }
      }
    },
    "/api/memberships": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Create membership",
        "description": "Creates an L3 membership joining an existing L2 app_identity to a tenant, returning 201 with the new row. Requires a valid tenant JWT (requireAuth). Edge cases: only presence of app_identity_id and tenant_id is validated - there is no UUID-format check, no existence check and no comparison against the caller JWT tenant, so a caller can create a membership in a tenant they do not belong to; empty strings count as missing and yield 400; a non-existent app_identity_id or tenant_id passes validation and then trips a foreign-key violation inside an untried service call, which the Fastify default error handler reports as 500 rather than 404 or 409; a malformed non-UUID id likewise becomes a 500; the endpoint is not idempotent - repeating the same pair either duplicates the membership or trips a unique constraint (again a 500) depending on the schema.",
        "operationId": "post_api_memberships",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app_identity_id": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "app_identity_id": "<id from app-identities>",
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "A signed-in person sees their memberships",
        "description": "Lists every tenant and app the signed-in person holds an active membership in — the read behind a single login that works across several providers. One email is one identity.person GLOBALLY (identity.alias is UNIQUE (kind, value_hash)), and that one person may hold memberships in any number of tenants, which is exactly what lets one credential open several providers apps. Until now nothing exposed that list: listMemberships() was internal and the only exposed read was scoped to a single app_identity, so a client had no way to learn the tenant_id it must send to obtain a scoped token, and an app-switcher could not be built at all. THE SUBJECT COMES FROM THE VERIFIED TOKEN AND THERE IS DELIBERATELY NO PARAMETER TO OVERRIDE IT: accepting a person_id would let any authenticated caller enumerate another persons provider relationships, which is precisely the cross-tenant disclosure the default-deny gate exists to prevent. Retired apps are omitted because a membership in one is a door that does not open. Each row carries the tenant, its app, the business unit, the role template and whether an app_identity has been minted yet — the app_identity mints itself on first per-app login, so false simply means they have not entered that app.",
        "operationId": "get_api_memberships",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/memberships/{membership_id}/personas": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List personas",
        "description": "Lists the L4 personas attached to one membership. Requires a valid tenant JWT (requireAuth). Edge cases: this is a list, so an unknown but well-formed membership_id returns 200 with an empty personas array - never a 404; there is no limit/offset paging, so every persona is returned in one payload; shredded personas are not filtered by the route, so the caller must inspect the returned state; the caller JWT tenant is never compared to the membership, so personas under another tenant membership are readable - test tenant scoping explicitly; a malformed non-UUID membership_id fails the uuid cast in an untried service call and surfaces as a Fastify 500 rather than a 400.",
        "operationId": "get_api_memberships_membership_id_personas",
        "parameters": [
          {
            "name": "membership_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "membership_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/memberships/{membership_id}/terminate": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Terminate membership",
        "description": "Terminates the L3 membership named by the membership_id path param and returns the updated row. Requires a valid tenant JWT (requireAuth). Edge cases: no body is read, so any payload is ignored; there is no state precondition - an already-terminated membership can be terminated again and still returns 200 with the row (repeatable, though the termination stamp is rewritten), whereas an unknown membership_id returns 404 NotFound; the caller JWT tenant is never compared to the membership, so any authenticated caller can terminate any membership - verify tenant scoping deliberately; the route does not cascade to or check dependent L4 personas; a malformed non-UUID membership_id fails the uuid cast in an untried service call and surfaces as a Fastify 500 rather than a 404.",
        "operationId": "post_api_memberships_membership_id_terminate",
        "parameters": [
          {
            "name": "membership_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "membership_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "terminate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/meter/assets/{asset_id}/usage": {
      "get": {
        "tags": [
          "sdk-meter"
        ],
        "summary": "Read per-robot metered usage",
        "description": "Returns the per-day metered usage rollup for one robot/sensor asset from meter.robot_usage_day, scoped to the tenant on the caller's JWT: rows of asset_id, sensor_id (NULL for asset-level usage), day, sku and units, ordered day DESC then sku ASC, wrapped in a {success, data} envelope. Edge cases: the tenant is read from req.auth.tenant_id and never from the request - a token with no tenant claim is rejected 400 before any query runs, and an asset owned by another tenant simply returns an empty array instead of 403; an unknown or never-metered asset_id likewise returns 200 with data: [] and no 404; results are hard-capped at LIMIT 5000 with no pagination cursor, so a long-lived asset silently truncates; both tenant_id and asset_id are cast ::uuid inside the SQL, so a non-UUID asset_id surfaces through the catch block as a 500 rather than a 400.",
        "operationId": "get_api_meter_assets_asset_id_usage",
        "parameters": [
          {
            "name": "asset_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "asset_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "usage_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "BadRequest — tenant context required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — <propagated database error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/meter/health": {
      "get": {
        "tags": [
          "sdk-meter"
        ],
        "summary": "Health probe returns emit-only mode marker",
        "description": "Liveness probe for the sdk-meter surface. Returns 200 with the static object {sdk: 'sdk-meter', mode: 'emit-only', status: 'ok'}; the handler takes no arguments, reads no request state and touches neither Postgres nor Kafka, so it confirms the meter routes are mounted rather than that its dependencies are healthy. Edge cases: the path ends in /health, so the gateway's default-deny authGate treats it as public - it answers identically with no Authorization header, an expired token or a garbage token and never returns 401; it accepts no query parameters and no body, so there is no validation, tenant-scoping, pagination or state precondition to exercise, and the handler has no client-error branch at all.",
        "operationId": "get_api_meter_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/mfa/challenge": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Issue a TOTP challenge",
        "description": "Issues a step-up MFA challenge for the CALLER (person_id is taken from the verified JWT sub, never from the body), returning 201 with challenge_id, kind, a kind-specific payload and expires_at. kind defaults to 'totp' and must be one of totp|webauthn|sms_otp; the payload is an instruction string for totp, a { challenge, allow_credentials } object for webauthn, and an sms delivery hint for sms_otp. Edge cases: challenges live in an in-process Map with a short TTL and are purged on each call, so they do not survive a gateway restart and are not shared across replicas; issuing repeatedly creates independent challenge_ids with no rate limit or per-person cap, and older ones stay valid until they expire; an empty body is accepted and silently defaults to totp. Requires a valid tenant JWT (route preHandler requireAuth plus the gateway default-deny gate).",
        "operationId": "post_api_mfa_challenge",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "challenge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "totp",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — kind must be totp|webauthn|sms_otp"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "kind": "totp"
              }
            }
          }
        }
      }
    },
    "/api/mfa/verify": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Verify a 6-digit TOTP response",
        "description": "Completes an MFA challenge issued by POST /api/mfa/challenge and returns 200 with { verified, person_id, mfa_level: 2 }, also stamping identity.credential.last_used_at for that person and credential kind. This path is deliberately PUBLIC — it is on the gateway authGate allowlist and has no requireAuth preHandler, because the caller is mid-login and has no full JWT yet. Both challenge_id and response are required. Edge cases: a challenge is SINGLE-USE — it is deleted from the in-process store on the first verify attempt whether or not it succeeded, so replaying the same challenge_id returns challenge_not_found_or_expired; challenges also expire on their TTL and are purged on every call, and do not survive a restart or reach another replica; for totp any 6-digit numeric string is accepted in dev, while webauthn/sms_otp require the exact issued secret; a failed verification is reported as 401 (not 200 with verified:false).",
        "operationId": "post_api_mfa_verify",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "challenge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "response": "123456",
                    "verify_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — challenge_id and response are required"
          },
          "401": {
            "description": "MfaFailed — challenge_not_found_or_expired"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "challenge_id": {
                    "type": "string"
                  },
                  "response": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "challenge_id": "<id from mfa>",
                "response": "123456"
              }
            }
          }
        }
      }
    },
    "/api/notifications/delivery-receipts": {
      "get": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "List delivery receipts",
        "description": "List a tenant's provider delivery-status receipts (newest first), optionally filtered by status. tenant_id query param required.",
        "operationId": "get_api_notifications_delivery_receipts",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "delivery_receipt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/notifications/dispatch": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Dispatch an email through the unified transport",
        "description": "Unified send transport that routes a message to the channel's provider chain with failover (email SES/SMTP, SMS Twilio) and defers on per-persona quiet hours. This is the single transport the sdk-sequence step sender uses. Returns {status: sent|deferred|failed, provider, provider_message_id}. tenant_id, channel, destination and body are required.",
        "operationId": "post_api_notifications_dispatch",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "dispatch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "destination": "qa.user@example.com",
                    "subject": "Your follow-up",
                    "body": "Thanks for your time — here are the next steps.",
                    "respect_quiet_hours": false,
                    "metadata": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, channel, destination and body are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "destination": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "respect_quiet_hours": {
                    "type": "boolean"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "destination": "<generated email>",
                "subject": "Your follow-up",
                "body": "Thanks for your time — here are the next steps.",
                "respect_quiet_hours": false,
                "metadata": {}
              }
            }
          }
        }
      }
    },
    "/api/notifications/providers": {
      "get": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "List the tenant's email providers (metadata only)",
        "description": "Lists the email provider bindings configured for the caller's tenant. The tenant is derived from the JWT, never from a query param, so this endpoint is inherently tenant-scoped and cannot be made to read another tenant's providers. Secrets are never returned — only binding metadata. Edge cases: a JWT with no tenant_id claim is 403 rather than 401; a tenant with no providers configured returns 200 with data.providers = [], not 404; there are no filter, limit or paging parameters, so every binding the service query selects is returned in one response.",
        "operationId": "get_api_notifications_providers",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "provider_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — <service error matching unsupported|too short|must be at least|not found|invalid>"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Configure a SendGrid email provider for the tenant",
        "description": "Binds the tenant's outbound email provider (BYO smtp | sendgrid | ses). The tenant comes from the JWT — there is NO tenant_id body field — and the credential is envelope-encrypted by the service and never echoed back in any response. Edge cases: a JWT with no tenant_id claim is 403 Forbidden, not 401; kind is whitelist-checked and credential must be at least 4 characters, and both failures are collected into a single 400; config defaults to {} and from_address is optional, so an SMTP binding with no host can be created and only fails later at verify/send time; fallback_on_error is honoured only when it is a real boolean; service rejections whose message matches unsupported|too short|must be at least|not found|invalid are mapped to 400 ValidationError.",
        "operationId": "post_api_notifications_providers",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "provider_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "sendgrid",
                    "from_address": "qa.user@example.com",
                    "credential": "SG.qa-test-api-key-abcd1234",
                    "config": {},
                    "fallback_on_error": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — kind must be one of smtp, sendgrid, ses / credential is required (min 4 chars)"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "from_address": {
                    "type": "string"
                  },
                  "credential": {
                    "type": "string"
                  },
                  "config": {
                    "type": "object",
                    "properties": {}
                  },
                  "fallback_on_error": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "kind": "sendgrid",
                "from_address": "<generated email>",
                "credential": "{{static:SG.qa-test-api-key-abcd1234}}",
                "config": {},
                "fallback_on_error": true
              }
            }
          }
        }
      }
    },
    "/api/notifications/providers/{provider_id}": {
      "delete": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Revoke the tenant's email provider",
        "description": "Revokes an email provider binding for the caller's tenant, recording an audit reason. The body is optional — an absent reason defaults to 'tenant revoked email provider'. The binding is resolved by (binding_id, tenant_id from the JWT), so another tenant's provider_id is unreachable. Edge cases: a JWT with no tenant_id claim is 403; an unknown, cross-tenant or ALREADY-revoked :provider_id raises a service 'not found' error that fail() turns into 400 ValidationError rather than 404, so a repeat delete is a 400 and this route is not idempotent; revoking the tenant's only provider leaves outbound email with no binding and is not blocked.",
        "operationId": "delete_api_notifications_providers_provider_id",
        "parameters": [
          {
            "name": "provider_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — <service error matching unsupported|too short|must be at least|not found|invalid>"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Rotate the tenant's email provider credential",
        "description": "Rotates an email provider binding's credential (and optionally its config). credential is MANDATORY on every call — this route cannot patch config alone — and must be at least 4 characters. The binding is resolved by (binding_id, tenant_id from the JWT), so a provider_id belonging to another tenant behaves as not-found. Edge cases: a JWT with no tenant_id claim is 403; an unknown or cross-tenant :provider_id raises a service 'not found' error that the fail() mapper pattern-matches to 400 ValidationError, NOT 404 — the surprising mapping to test; omitting config leaves the stored config untouched; the new credential is envelope-encrypted and never returned.",
        "operationId": "patch_api_notifications_providers_provider_id",
        "parameters": [
          {
            "name": "provider_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "provider_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "credential": "SG.qa-rotated-api-key-wxyz9876",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — credential is required (min 4 chars)"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "credential": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "credential": "{{static:SG.qa-rotated-api-key-wxyz9876}}"
              }
            }
          }
        }
      }
    },
    "/api/notifications/providers/{provider_id}/verify": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Verify the configured email provider with a test send",
        "description": "Verifies an email provider binding by sending a real test message to the `to` address. CRITICAL for testing: a FAILED test send is treated as an expected, informative outcome and STILL returns 200 — with data.verified=false and data.error carrying the provider's message — so callers must inspect data.verified, not the status code. Only the missing-recipient and missing-tenant-claim cases produce non-200 responses. Edge cases: `to` is required and trimmed but is NOT format-validated, so a syntactically invalid address reaches the provider and returns verified:false; an unknown or cross-tenant :provider_id likewise surfaces as verified:false rather than 404; the call sends actual email, so it is not free to retry.",
        "operationId": "post_api_notifications_providers_provider_id_verify",
        "parameters": [
          {
            "name": "provider_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "to": "qa.user@example.com",
                    "verify_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — to (recipient email) is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "to": "<generated email>"
              }
            }
          }
        }
      }
    },
    "/api/notifications/quiet-hours": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Set nightly 22:00-06:00 PT quiet window",
        "description": "Upserts a persona's quiet-hours windows and do-not-disturb flag; the sender defers (suppresses) messages that fall inside a window. Idempotent by persona — re-posting REPLACES the previous window set, so posting windows:[] clears quiet hours entirely rather than erroring. Edge cases: a JWT with no tenant_id claim is 403, not 401; every window must be {dow:0-6, start:'HH:MM', end:'HH:MM', tz} and ONE malformed element rejects the whole array; start/end are regex-checked for HH:MM SHAPE only, so '99:99' passes validation; tz is only checked for non-empty, so an invalid IANA zone is not caught here; an overnight window (start > end) is accepted; dnd is optional and independent of the windows.",
        "operationId": "post_api_notifications_quiet_hours",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "quiet_hour_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "windows": [
                      {
                        "dow": 0,
                        "start": "22:00",
                        "end": "06:00",
                        "tz": "America/Los_Angeles"
                      },
                      {
                        "dow": 1,
                        "start": "22:00",
                        "end": "06:00",
                        "tz": "America/Los_Angeles"
                      }
                    ],
                    "dnd": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — persona_id is required / windows must be an array / each window needs {dow:0-6, start:HH:MM, end:HH:MM, tz}"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persona_id": {
                    "type": "string"
                  },
                  "windows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "dow": {
                          "type": "integer"
                        },
                        "start": {
                          "type": "string"
                        },
                        "end": {
                          "type": "string"
                        },
                        "tz": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "dnd": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "persona_id": "<id from auth>",
                "windows": [
                  {
                    "dow": 0,
                    "start": "22:00",
                    "end": "06:00",
                    "tz": "America/Los_Angeles"
                  },
                  {
                    "dow": 1,
                    "start": "22:00",
                    "end": "06:00",
                    "tz": "America/Los_Angeles"
                  }
                ],
                "dnd": false
              }
            }
          }
        }
      }
    },
    "/api/notifications/send": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Send email notification with rendered body",
        "description": "Sends a templated notification through the full pipeline: consent check -> quiet-hours check -> template render -> queue. tenant_id is OVERWRITTEN from the caller's JWT, so a tenant_id in the body is ignored and cross-tenant sends are impossible here. The status code encodes the outcome: 201 when queued but 200 when the message was SUPPRESSED (consent absent/revoked, inside a quiet-hours window, or dnd) — a 200 therefore means nothing was sent and the reason is in data.suppression_reason. Edge cases: a JWT with no tenant_id claim is 403, not 401; template_code must resolve to a template for that (tenant, channel) or the send is 404 TemplateNotFound; enforce_consent and honor_quiet_hours let the caller opt out of those gates; scheduled_at defers delivery.",
        "operationId": "post_api_notifications_send",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "send_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "template_code": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "destination": "qa.user@example.com",
                    "payload": {
                      "name": "Alice"
                    },
                    "locale": "en-US",
                    "scheduled_at": "2026-01-15T10:30:00Z",
                    "enforce_consent": false,
                    "honor_quiet_hours": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required / template_code is required / person_id is required / channel must be one of email, sms, whatsapp, push, slack / destination is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "TemplateNotFound — Template <code> not found for channel <channel>"
          },
          "409": {
            "description": "Conflict — Template already exists for that (tenant, code, channel, version)"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "template_code": {
                    "type": "string"
                  },
                  "person_id": {
                    "type": "string"
                  },
                  "app_identity_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "destination": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  "locale": {
                    "type": "string"
                  },
                  "scheduled_at": {
                    "type": "string"
                  },
                  "enforce_consent": {
                    "type": "boolean"
                  },
                  "honor_quiet_hours": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "template_code": "<id from notifications>",
                "person_id": "<id from auth>",
                "app_identity_id": "<id from auth>",
                "channel": "email",
                "destination": "<generated email>",
                "payload": {
                  "name": "Alice"
                },
                "locale": "en-US",
                "scheduled_at": "<generated futuredatetime>",
                "enforce_consent": false,
                "honor_quiet_hours": false
              }
            }
          }
        }
      }
    },
    "/api/notifications/sms-consent": {
      "get": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "List consent states",
        "description": "List a tenant's SMS consent states (newest first), optionally filtered by status (opted_in/opted_out). Numbers are shown as last-4 only. tenant_id query param required.",
        "operationId": "get_api_notifications_sms_consent",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "sms_consent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Opt a number out (STOP)",
        "description": "Propagate an SMS opt-out (STOP) or opt-in (START) to BOTH the suppression list (reason-tagged, via sdk-deliverability) AND the local consent record, emitting a PII-safe opt-out/opt-in event. Idempotent per (tenant, number): a duplicate leaves state unchanged (changed=false). The number is stored only as a sha256 hash + last 4 digits (never plaintext). tenant_id, phone and action are required.",
        "operationId": "post_api_notifications_sms_consent",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sms_consent_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "phone": "+1555{{dynamic:number}}",
                    "action": "opt_out",
                    "source": "api",
                    "purpose": "marketing",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, phone and action are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "phone": "+1555{{dynamic:number}}",
                "action": "opt_out",
                "source": "api",
                "purpose": "marketing"
              }
            }
          }
        }
      }
    },
    "/api/notifications/sms-inbound": {
      "get": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "List inbound SMS",
        "description": "List a tenant's inbound SMS (newest first), optionally filtered by keyword intent (opt_out/opt_in/help/none). tenant_id query param required.",
        "operationId": "get_api_notifications_sms_inbound",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "sms_inbound_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/notifications/sms-settings": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Set the HELP auto-reply",
        "description": "Configure a tenant's inbound-SMS settings: the HMAC signing secret used to verify Twilio webhooks and the HELP/opt-out/opt-in auto-reply text. Upsert per tenant. tenant_id required.",
        "operationId": "post_api_notifications_sms_settings",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sms_setting_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "help_reply": "Support: reply STOP to opt out, START to opt in. Call 1-800-555-0100.",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "help_reply": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "help_reply": "Support: reply STOP to opt out, START to opt in. Call 1-800-555-0100."
              }
            }
          }
        }
      }
    },
    "/api/notifications/templates": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Register a tenant-scoped email template",
        "description": "Creates a notification template. tenant_id is FORCED from the caller's JWT unless the body sets platform_default:true, in which case the body is used verbatim to create a platform-wide default. Edge cases: a JWT with no tenant_id claim is 403 rather than 401; locale_bundles is required and must contain AT LEAST ONE locale — an empty object {} is rejected; channel is whitelist-checked against email/sms/whatsapp/push/slack; version is optional free text; the (tenant, code, channel, version) tuple is unique, so re-creating the same template returns 409 Conflict rather than upserting — bump version to publish a revision; template placeholders are not validated against any payload schema at create time.",
        "operationId": "post_api_notifications_templates",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "code": "sample-slug",
                    "channel": "email",
                    "locale_bundles": {
                      "en-US": {
                        "subject": "Welcome",
                        "body": "Hi {name}, welcome!"
                      }
                    },
                    "required_consent_purpose": null,
                    "version": "1.0.0",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — code is required / channel must be one of email, sms, whatsapp, push, slack / locale_bundles is required and must include at least one locale"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "TemplateNotFound — Template not found"
          },
          "409": {
            "description": "Conflict — Template already exists for that (tenant, code, channel, version)"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "locale_bundles": {
                    "type": "object",
                    "properties": {
                      "en-US": {
                        "type": "object",
                        "properties": {
                          "subject": {
                            "type": "string"
                          },
                          "body": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "required_consent_purpose": {
                    "nullable": true
                  },
                  "version": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "code": "<generated slug>",
                "channel": "email",
                "locale_bundles": {
                  "en-US": {
                    "subject": "Welcome",
                    "body": "Hi {name}, welcome!"
                  }
                },
                "required_consent_purpose": null,
                "version": "1.0.0"
              }
            }
          }
        }
      }
    },
    "/api/notifications/webhooks/delivery/{provider}": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Twilio delivered callback (unmatched id handled gracefully)",
        "description": "PUBLIC provider delivery-status webhook (Twilio/SES/SendGrid, on the gateway allowlist). Normalizes the status (delivered/failed/bounced/undelivered/complaint), looks up the notification.message by provider_message_id and drives sent->delivered via markDelivered (emitting notification.delivered.v1 once), records an idempotent receipt (per provider+message_id+status), and feeds delivered/bounce counts to reputation. Unknown message ids are recorded gracefully (matched=false). tenant_id via ?tenant_id=. Signature-verified when a per-tenant secret is configured.",
        "operationId": "post_api_notifications_webhooks_delivery_provider",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "delivery_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "MessageStatus": "delivered",
                    "MessageSid": "SM{{dynamic:uuid}}",
                    "To": "+15005550006",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "InvalidSignature — delivery callback signature verification failed"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "MessageStatus": {
                    "type": "string"
                  },
                  "MessageSid": {
                    "type": "string"
                  },
                  "To": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "MessageStatus": "delivered",
                "MessageSid": "SM{{dynamic:uuid}}",
                "To": "+15005550006"
              }
            }
          }
        }
      }
    },
    "/api/notifications/webhooks/sms/inbound": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Inbound STOP opts the number out",
        "description": "PUBLIC inbound SMS webhook (Twilio, on the gateway allowlist). HMAC-verified when a signing secret is configured for the tenant, else accepted. Classifies the leading keyword case-insensitively: STOP/UNSUBSCRIBE/CANCEL/END/QUIT -> opt_out (suppresses the number), START/UNSTOP/YES -> opt_in (resubscribes), HELP/INFO -> returns the configured auto-reply; unknown text is a no-op. Idempotent per (provider, MessageSid). tenant_id via ?tenant_id=.",
        "operationId": "post_api_notifications_webhooks_sms_inbound",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "inbound_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "From": "+1555{{dynamic:number}}",
                    "To": "+15005550006",
                    "Body": "STOP",
                    "MessageSid": "SM{{dynamic:uuid}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "InvalidSignature — inbound SMS signature verification failed"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "From": {
                    "type": "string"
                  },
                  "To": {
                    "type": "string"
                  },
                  "Body": {
                    "type": "string"
                  },
                  "MessageSid": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "From": "+1555{{dynamic:number}}",
                "To": "+15005550006",
                "Body": "STOP",
                "MessageSid": "SM{{dynamic:uuid}}"
              }
            }
          }
        }
      }
    },
    "/api/offers": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Create an offer",
        "description": "Create a stable offer identity (name + unique slug); content lives in immutable versions. Slug is unique per tenant (duplicate -> 409). tenant_id, name and slug required.",
        "operationId": "post_api_offers",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "offer_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "slug": "offer-{{dynamic:uuid}}",
                    "description": "Pro plan",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, name and slug are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "409": {
            "description": "Conflict — an offer with this slug already exists"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "slug": "offer-{{dynamic:uuid}}",
                "description": "Pro plan"
              }
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}/check-reference": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Current reference is not stale",
        "description": "The stale-reference guard: given a pinned offer_version_id, report whether it is still the current version. A reference to a superseded (retired/older) version is stale=true. Current references pass (stale=false). Callable by CRM at quote/version-stamp time. tenant_id and offer_version_id required.",
        "operationId": "post_api_offers_offer_id_check_reference",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_reference_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "offer_version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and offer_version_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "offer_version_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "offer_version_id": "<id from offers>"
              }
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}/current": {
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Resolve current (live after activate)",
        "description": "Resolve the current version for an offer with a fallback chain: the live version if present, else the most recent beta, else the most recent draft. Returns {version, source} (source=live|beta|draft|none). tenant_id query param required.",
        "operationId": "get_api_offers_offer_id_current",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "current_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/offers/{offer_id}/version-stamp": {
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Stamp the current version",
        "description": "Return the offer version a consumer should pin (version-stamp): the current version via resolve (live, else beta, else draft). A record stores offer_version_id and later revalidates with check-reference. 404 if the offer has no version. tenant_id query param required.",
        "operationId": "get_api_offers_offer_id_version_stamp",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "version_stamp_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — offer has no current version to stamp"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/offers/{offer_id}/versions": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Create a draft version",
        "description": "Create an immutable offer version (starts in status 'draft'). version is unique per (tenant, offer). tenant_id and version required.",
        "operationId": "post_api_offers_offer_id_versions",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "version": "v-{{dynamic:uuid}}",
                    "title": "Pro v1",
                    "price": 49,
                    "currency": "USD",
                    "body": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and version are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "409": {
            "description": "Conflict — this version already exists for the offer"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "price": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "body": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "version": "v-{{dynamic:uuid}}",
                "title": "Pro v1",
                "price": 49,
                "currency": "USD",
                "body": {}
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "List the versions of the offer just created",
        "description": "Every version of one offer, returned as data.versions. tenant_id is a REQUIRED QUERY PARAMETER - listOfferVersions scopes by (tenant_id, offer_id) - so omitting it is 400. Unlike GET /api/offers/:offer_id this route does NOT 404 on an unknown offer_id: an offer that does not exist and an offer with no versions yet both answer 200 with an empty array, because the handler never checks the parent exists. A caller that needs to distinguish those two must read the offer first. The list is the full version history including superseded ones, not just the live version - exactly one version is live at a time and activate atomically demotes the prior one, so read status rather than assuming order.",
        "operationId": "get_api_offers_offer_id_versions",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/offers/{offer_id}/versions/{version_id}/activate": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Activate the draft version",
        "description": "Activate/publish a version: atomically demote the prior live version for this offer (-> retired) and promote the target (-> live, activated_at now), emitting offer_catalog.version.activated.v1. At most one live version per offer. tenant_id required.",
        "operationId": "post_api_offers_offer_id_versions_version_id_activate",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "activate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — offer version not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}/versions/{version_id}/features": {
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "List the feature matrix",
        "description": "Read a version's feature-status matrix (by sort order then key). Each row is one feature's status within that immutable version. tenant_id query param required.",
        "operationId": "get_api_offers_offer_id_versions_version_id_features",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "feature_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Mark a feature included",
        "description": "Set (upsert) one feature's status within an immutable offer version — the feature-status matrix cell. Idempotent per (version, feature_key). tenant_id, feature_key and name required; status defaults to 'included'.",
        "operationId": "post_api_offers_offer_id_versions_version_id_features",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "feature_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "feature_key": "api-access",
                    "name": "API Access",
                    "status": "included",
                    "value": "unlimited",
                    "sort_order": 1,
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, feature_key and name are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — offer version not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "feature_key": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  },
                  "sort_order": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "feature_key": "api-access",
                "name": "API Access",
                "status": "included",
                "value": "unlimited",
                "sort_order": 1
              }
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}/versions/{version_id}/publish-decision": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Approve the publish request",
        "description": "Record the sdk-approval decision for a version's publish request: approved (activation now permitted) or rejected (activation stays blocked). tenant_id and decision required.",
        "operationId": "post_api_offers_offer_id_versions_version_id_publish_decision",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "publish_decision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "decision": "approved",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and decision are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — offer version not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "decision": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "decision": "approved"
              }
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}/versions/{version_id}/publish-request": {
      "post": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Request publish approval",
        "description": "File a publish-approval request for an offer version (delegated to sdk-approval; subject = offer_version_id). Sets approval_status='pending' and stores the approval_ref. While pending (or rejected) the version cannot be activated (409). tenant_id required.",
        "operationId": "post_api_offers_offer_id_versions_version_id_publish_request",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "publish_request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — offer version not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/ontology/{id}/deprecate": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Deprecate a registered ontology version",
        "description": "Deprecates a specific ontology version by id with an optional reason, returning the updated ontology record. Edge cases: reason is optional and defaults to the literal \"unspecified\" when the body omits it, so a deprecation is never blocked for lack of justification; every error from deprecateOntology maps to 404 with the raw message, so an unknown id, an id already deprecated, and a datastore failure all present as 404 and must be told apart by message text; because a repeat call on an already-deprecated version raises rather than no-ops, the operation is not idempotent; deprecating the active version leaves the ontology name with no active version, after which GET /ontology/:name/active returns 404.",
        "operationId": "post_ontology_id_deprecate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "deprecate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "superseded by healthcare-core 2.0.0",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error message from deprecateOntology>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "superseded by healthcare-core 2.0.0"
              }
            }
          }
        }
      }
    },
    "/ontology": {
      "get": {
        "tags": [
          "semantic-service"
        ],
        "summary": "List all registered ontologies",
        "description": "Lists every registered ontology known to the semantic service, returning the full collection in one response. Edge cases: the handler takes no query parameters — there is no filtering by name, tenant or status and no pagination or limit, so active and deprecated versions all come back together and the payload grows unbounded with the number of registered ontologies; an empty registry returns 200 with an empty data array rather than 404; the route has no validation branch and no not-found branch, so the only reachable failure is an unhandled datastore error surfacing through the Fastify error handler as 500.",
        "operationId": "get_ontology",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "ontology_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/ontology/{name}/active": {
      "get": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Fetch the active ontology by name (seeded always-active ontology)",
        "description": "Fetches the currently active ontology version for a given ontology name from the path. Edge cases: any error thrown by getActiveOntology is mapped to 404 with the raw message, so a name that was never registered and a name whose every version has been deprecated (leaving no active version) both return 404 and are only distinguishable by message text; a datastore failure is also swallowed into that 404 branch rather than surfacing as 500, so a 404 here does not strictly prove absence; the lookup is by name only with no version or tenant qualifier, so it always reflects the most recent activation.",
        "operationId": "get_ontology_name_active",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "name path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "active_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error message from getActiveOntology>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/ontology/register": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Register a self-contained domain ontology bundle (activate on register)",
        "description": "Registers a DomainOntologyBundle under a bundle_ref and, unless activate is explicitly false, makes it the active version for its ontology name — activate defaults to true, so an ordinary register call silently supersedes whatever version was previously active. Edge cases: both bundle and bundle_ref are required and a missing one is a 400; every downstream failure from registerOntology is also collapsed to 400 with the raw error message rather than a typed status, so a structurally invalid bundle, an unresolvable reference inside it, and a duplicate bundle_ref all present as 400 and must be distinguished by message text; re-registering an identical bundle_ref is therefore not a safe idempotent retry; the route performs no tenant scoping of its own.",
        "operationId": "post_ontology_register",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "register_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "bundle": {
                      "name": "healthcare-core",
                      "version": "1.0.0",
                      "parent_ontology": null,
                      "object_types": [
                        {
                          "name": "Patient",
                          "attribute_schema": {
                            "mrn": "string"
                          },
                          "backed_by": "persona.persona_ext:patient_chart"
                        },
                        {
                          "name": "Encounter",
                          "attribute_schema": {
                            "code": "string"
                          },
                          "backed_by": "clinical.encounter"
                        }
                      ],
                      "relation_types": [
                        {
                          "name": "treats",
                          "cardinality": "1:N",
                          "rebac_kind_mapping": "member",
                          "from_object_type_name": "Patient",
                          "to_object_type_name": "Encounter"
                        }
                      ],
                      "capability_graph": [
                        {
                          "tool_sku": "clinical.note.summarize",
                          "pre_conditions": {},
                          "post_conditions": {},
                          "object_type_name": "Patient",
                          "requires_relation_name": "treats"
                        }
                      ]
                    },
                    "bundle_ref": "@projexlight/contracts@3.1.0",
                    "activate": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — bundle and bundle_ref required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bundle": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "version": {
                        "type": "string"
                      },
                      "parent_ontology": {
                        "nullable": true
                      },
                      "object_types": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "attribute_schema": {
                              "type": "object",
                              "properties": {
                                "mrn": {
                                  "type": "string"
                                }
                              }
                            },
                            "backed_by": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "relation_types": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "name": {
                              "type": "string"
                            },
                            "cardinality": {
                              "type": "string"
                            },
                            "rebac_kind_mapping": {
                              "type": "string"
                            },
                            "from_object_type_name": {
                              "type": "string"
                            },
                            "to_object_type_name": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "capability_graph": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "tool_sku": {
                              "type": "string"
                            },
                            "pre_conditions": {
                              "type": "object",
                              "properties": {}
                            },
                            "post_conditions": {
                              "type": "object",
                              "properties": {}
                            },
                            "object_type_name": {
                              "type": "string"
                            },
                            "requires_relation_name": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  },
                  "bundle_ref": {
                    "type": "string"
                  },
                  "activate": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "bundle": {
                  "name": "healthcare-core",
                  "version": "1.0.0",
                  "parent_ontology": null,
                  "object_types": [
                    {
                      "name": "Patient",
                      "attribute_schema": {
                        "mrn": "string"
                      },
                      "backed_by": "persona.persona_ext:patient_chart"
                    },
                    {
                      "name": "Encounter",
                      "attribute_schema": {
                        "code": "string"
                      },
                      "backed_by": "clinical.encounter"
                    }
                  ],
                  "relation_types": [
                    {
                      "name": "treats",
                      "cardinality": "1:N",
                      "rebac_kind_mapping": "member",
                      "from_object_type_name": "Patient",
                      "to_object_type_name": "Encounter"
                    }
                  ],
                  "capability_graph": [
                    {
                      "tool_sku": "clinical.note.summarize",
                      "pre_conditions": {},
                      "post_conditions": {},
                      "object_type_name": "Patient",
                      "requires_relation_name": "treats"
                    }
                  ]
                },
                "bundle_ref": "@projexlight/contracts@3.1.0",
                "activate": true
              }
            }
          }
        }
      }
    },
    "/api/participants/{participant_id}/leave": {
      "post": {
        "tags": [
          "sdk-engagement"
        ],
        "summary": "A participant leaves the encounter (sets left_at)",
        "description": "Marks an encounter participant as having left by stamping engagement.encounter_participant.left_at = now(), returning 200 with the updated participant row (participant_id, encounter_id, persona_id, role, joined_at, left_at, required). The UPDATE is guarded by `left_at IS NULL`, so only currently-joined participants are affected. Edge cases: leaving twice returns 404 on the second call because zero rows match, as does an unknown participant_id; the handler does not check the parent encounter's state, so a participant can leave an encounter that is already closed or sealed; there is no re-join endpoint, and required=true participants are not protected from leaving; a non-UUID participant_id fails the Postgres cast as an uncaught 500. Requires a valid tenant JWT.",
        "operationId": "post_api_participants_participant_id_leave",
        "parameters": [
          {
            "name": "participant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "participant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "leave_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/payments/charge": {
      "post": {
        "tags": [
          "sdk-payment"
        ],
        "summary": "Charge $25.00 USD",
        "description": "Captures a charge against a stored payment method via the provider. Requires a valid tenant JWT (requireAuth); tenant_id is forced from the JWT and any body tenant_id is ignored. The success status is conditional: 201 when the provider result is \"captured\", 200 for any other terminal status (for example pending or authorized) - do not assert 201 unconditionally. Edge cases: a JWT with no tenant_id claim is a 403, not a 500; amount must be a finite positive number, so 0, a negative, a string and NaN all 400; currency is length-checked only (exactly 3 characters) and is not validated against the real ISO-4217 list, so \"XXX\" passes; method_id is presence-checked only, so an unknown method_id becomes a 404 PaymentMethodNotFound from the service, and a method owned by another tenant is a 403 TenantOwnership rather than a 404; a method or charge in a state that cannot be charged surfaces as a 409 InvalidState.",
        "operationId": "post_api_payments_charge",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "charge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "amount": 25,
                    "currency": "USD",
                    "encounter_id": "{{var:encounter_id}}",
                    "idempotency_key": "sample-slug",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required / method_id is required / amount must be a positive number / currency must be ISO-4217 3-letter"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "PaymentMethodNotFound — Payment method <method_id> not found"
          },
          "409": {
            "description": "InvalidState — <state error message>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "method_id": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "integer"
                  },
                  "currency": {
                    "type": "string"
                  },
                  "encounter_id": {
                    "type": "string"
                  },
                  "idempotency_key": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "method_id": "<id from payments>",
                "amount": 25,
                "currency": "USD",
                "encounter_id": "{{var:encounter_id}}",
                "idempotency_key": "<generated slug>"
              }
            }
          }
        }
      }
    },
    "/api/payments/{charge_id}/distribute": {
      "post": {
        "tags": [
          "sdk-payment"
        ],
        "summary": "Split charge between two parties",
        "description": "Appends immutable, hash-chained distribution ledger entries splitting a captured charge among parties. The caller tenant is taken from the JWT (never the body); the body charge_id is overwritten with the path param. Requires a 3-letter ISO-4217 currency and a non-empty splits[] where each split has a string party_persona_id and a positive-number share. The charge must exist, be owned by the caller tenant, and be in status \"captured\"; the batch share sum and the cumulative-of-all-prior-distributions must not exceed the charge amount. Edge cases: cross-tenant charge, missing charge, non-captured status, zero/negative shares, empty splits, wrong-length currency, over-subscription.",
        "operationId": "post_api_payments_charge_id_distribute",
        "parameters": [
          {
            "name": "charge_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "charge_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "distribute_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "currency": "USD",
                    "splits": [
                      {
                        "party_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                        "share": 20
                      },
                      {
                        "party_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                        "share": 5
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — charge_id required / currency must be ISO-4217 3-letter / splits must be a non-empty array / splits[i].party_persona_id required / splits[i].share must be a positive number"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "ChargeNotFound — Charge <charge_id> not found"
          },
          "409": {
            "description": "DistributionOversubscribed — cumulative distribution would exceed charge amount"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "currency": {
                    "type": "string"
                  },
                  "splits": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "party_persona_id": {
                          "type": "string"
                        },
                        "share": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "currency": "USD",
                "splits": [
                  {
                    "party_persona_id": "<id from auth>",
                    "share": 20
                  },
                  {
                    "party_persona_id": "<id from personas>",
                    "share": 5
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/payments/{charge_id}/refund": {
      "post": {
        "tags": [
          "sdk-payment"
        ],
        "summary": "Refund $5.00 USD below approval threshold",
        "description": "Issues a refund against the charge named by the charge_id path param; high-value refunds are gated by sdk-approval. Requires a valid tenant JWT (requireAuth); the caller tenant comes from the JWT and is passed to the service, which enforces ownership. Edge cases: a JWT without a tenant_id claim is a 403 rather than a 500; amount must be a finite positive number and reason is mandatory, so a 0 or negative amount and a missing reason each 400; refunding more than the remaining refundable balance (including the cumulative effect of earlier partial refunds) is a 422 InsufficientRefundableAmount, distinct from the 400 validation path; an unknown charge_id is a 404 ChargeNotFound and a charge owned by a different tenant is a 403 TenantOwnership, not a 404; a charge in a status that cannot be refunded (never captured, already fully refunded, voided) is a 409 InvalidState; the endpoint is not idempotent - repeating the same request issues a second refund until the refundable balance is exhausted.",
        "operationId": "post_api_payments_charge_id_refund",
        "parameters": [
          {
            "name": "charge_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "charge_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "refund_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "amount": 5,
                    "reason": "customer requested partial refund",
                    "approval_threshold": 10000,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — amount must be a positive number / reason is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "ChargeNotFound — Charge <charge_id> not found"
          },
          "409": {
            "description": "InvalidState — <state error message>"
          },
          "422": {
            "description": "InsufficientRefundableAmount — refund amount exceeds the remaining refundable balance"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "approval_threshold": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "amount": 5,
                "reason": "customer requested partial refund",
                "approval_threshold": 10000
              }
            }
          }
        }
      }
    },
    "/api/payments/methods": {
      "post": {
        "tags": [
          "sdk-payment"
        ],
        "summary": "Attach Stripe card via tokenized ref",
        "description": "Attaches a tokenized payment method to a persona and returns 201 with the stored method. Requires a valid tenant JWT (requireAuth); tenant_id is forced from the JWT tenant_id claim and any body tenant_id is overwritten, so a method cannot be attached to another tenant. Edge cases: a JWT without a tenant_id claim (for example a login with no tenant selected) is a 403 rather than a 500; provider must be one of the allowed providers and kind one of the allowed kinds, so an unknown value is a 400 listing the permitted set; provider_token is required and is additionally screened for raw-card-number shape - sending a PAN instead of a provider token is refused per FR-PAY-2 as a 400 and nothing is stored; persona_id is presence-checked only, so a non-existent persona surfaces from the service rather than the validator; validation accumulates, so one 400 may list several messages in details[]; the endpoint is not idempotent - re-posting the same provider_token attaches another method.",
        "operationId": "post_api_payments_methods",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "provider": "stripe",
                    "provider_token": "pm_card_visa_{{dynamic:slug}}",
                    "kind": "card",
                    "last4": "4242",
                    "brand": "visa",
                    "exp_month": 12,
                    "exp_year": 2030,
                    "secure_data_field_ref": "{{var:secure_data_field_ref}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required / persona_id is required / provider must be one of <list> / provider_token is required / kind must be one of <list> / provider_token looks like a raw card number; refusing per FR-PAY-2 (use the provider token only)"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "provider": {
                    "type": "string"
                  },
                  "provider_token": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "last4": {
                    "type": "string"
                  },
                  "brand": {
                    "type": "string"
                  },
                  "exp_month": {
                    "type": "integer"
                  },
                  "exp_year": {
                    "type": "integer"
                  },
                  "secure_data_field_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from auth>",
                "provider": "stripe",
                "provider_token": "pm_card_visa_{{dynamic:slug}}",
                "kind": "card",
                "last4": "4242",
                "brand": "visa",
                "exp_month": 12,
                "exp_year": 2030,
                "secure_data_field_ref": "{{var:secure_data_field_ref}}"
              }
            }
          }
        }
      }
    },
    "/api/personas": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Create patient persona",
        "description": "Creates an L4 persona under an existing L3 membership. membership_id and kind are mandatory; primary_role_template_id, bu_id and persona_key_ref are optional. Edge cases: only presence is validated — a membership_id that is not a UUID, or a valid UUID matching no membership row, is not caught by the handler and fails downstream in Postgres (uuid cast / FK violation) as a 500, not a 400/404; the same applies to primary_role_template_id and bu_id; kind is free text at this layer, so an unknown kind is only rejected if the DB enum rejects it; there is no duplicate-persona guard.",
        "operationId": "post_api_personas",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "membership_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "patient",
                    "primary_role_template_id": "{{var:role_template_id}}",
                    "bu_id": "{{var:bu_id}}",
                    "persona_key_ref": "{{var:persona_key_ref}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "membership_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "primary_role_template_id": {
                    "type": "string"
                  },
                  "bu_id": {
                    "type": "string"
                  },
                  "persona_key_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "membership_id": "<id from memberships>",
                "kind": "patient",
                "primary_role_template_id": "{{var:role_template_id}}",
                "bu_id": "{{var:bu_id}}",
                "persona_key_ref": "{{var:persona_key_ref}}"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List tenant members for a tenant_id",
        "description": "Gateway-composed member list for a tenant: reads identity.tenant_membership (status='active' only) and resolves each member's display name from the L2 profile band, falling back to the person's email alias and finally the literal 'Member'. Returns membership_id as persona_id plus role_template_id, bu_id and status. Edge cases: ?tenant_id= is REQUIRED (400 if absent) and is caller-asserted from the query string rather than the JWT; suspended/terminated members are silently excluded; results are hard-capped at LIMIT 500 with no paging cursor, so tenants above 500 active members are truncated with no indication; a tenant_id that is not a UUID fails the ::uuid cast and returns 500.",
        "operationId": "get_api_personas",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/personas/{persona_id}/bu": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Set persona business unit",
        "description": "Assigns (or clears) the business unit on a persona. bu_id is optional: omitting it or sending null CLEARS the persona's BU rather than erroring — there is no validation branch on this route at all. Edge cases: an unknown :persona_id updates zero rows and still returns {success:true}, so this endpoint never reports 404 and callers cannot detect a typo'd persona; a non-UUID :persona_id or a bu_id matching no BU row fails in Postgres (cast / FK) and returns 500; the BU is not checked to belong to the persona's tenant.",
        "operationId": "post_api_personas_persona_id_bu",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "bu_id": "{{var:bu_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bu_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "bu_id": "{{var:bu_id}}"
              }
            }
          }
        }
      }
    },
    "/api/personas/{persona_id}/deactivate": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Deactivate persona",
        "description": "Deactivates a persona by setting persona.status = 'suspended'. Takes no body. Edge cases: idempotent — suspending an already-suspended persona is a no-op that still returns {success:true}; the UPDATE row count is not checked, so an unknown :persona_id ALSO returns {success:true} and this endpoint can never report 404; a non-UUID :persona_id fails the query and returns 500; there is no re-activation counterpart on this route.",
        "operationId": "post_api_personas_persona_id_deactivate",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "deactivate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/personas/{persona_id}/role": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Set persona role label",
        "description": "Sets a persona's primary role. IMPORTANT: persona.persona stores the primary role as a role_template_id UUID — there is no free-text role column — so the `role` body field must be a role_template_id and a human-readable label such as 'admin' is rejected 400. Edge cases: a missing/empty role is 400 'role required'; a non-UUID role is a 400 with a distinct message; a well-formed UUID matching no role_template row violates the FK and surfaces as 500; the UPDATE row count is not checked, so an unknown :persona_id still returns {success:true} — this endpoint can never report 404.",
        "operationId": "post_api_personas_persona_id_role",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "role_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — role required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "role": "<id from role-templates>"
              }
            }
          }
        }
      }
    },
    "/api/personas/{persona_id}": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Get persona",
        "description": "Fetches a single persona by id. Edge cases: an unknown persona_id returns 404 NotFound; a :persona_id that is not a valid UUID is not validated by the handler and fails in Postgres as a 500; the lookup is by id alone and is NOT filtered by the caller's tenant, so any authenticated caller holding a persona_id can read it.",
        "operationId": "get_api_personas_persona_id",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/personas/{persona_id}/roles": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List persona roles",
        "description": "Lists the role assignments held by a persona. Edge cases: this is a list endpoint, so an unknown persona_id returns 200 with data.roles = [] rather than 404 — a typo'd persona is indistinguishable from a persona with no roles; a non-UUID :persona_id is not validated and fails in Postgres as a 500; there is no paging or limit parameter and no tenant filter on the read.",
        "operationId": "get_api_personas_persona_id_roles",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "role_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/personas/{persona_id}/shred": {
      "post": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "Shred persona independent of person",
        "description": "Crypto-shreds a persona (GDPR erasure): destroys the persona key reference so persona-scoped ciphertext becomes unreadable, returning the resulting persona row. Edge cases: an unknown :persona_id returns 404; a non-UUID :persona_id fails in Postgres as a 500; shredding is destructive and NOT reversible, yet the route has no already-shredded guard, so a repeat call still returns 200 with the row (idempotent in effect, not by check); the persona row itself is retained — only the key material is destroyed.",
        "operationId": "post_api_personas_persona_id_shred",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "shred_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/persons/{person_id}/app-identities": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List app identities",
        "description": "Lists every L2 app identity linked to one L1 person from persona.app_identity - app_identity_id, person_id, app_id, status, merged_into_app_identity_id and created_at - ordered by created_at ascending. Edge cases: an unknown person_id returns 200 with app_identities: [] rather than 404, so a non-existent person is indistinguishable from a person with no apps; person_id is a UUID column, so a non-UUID path segment fails at the database and surfaces as a 500 rather than a 400; merged identities are NOT filtered out - rows carrying a non-null merged_into_app_identity_id are still returned and callers must follow the merge pointer themselves; the result is unpaginated and the query filters on person_id alone, so the caller's JWT tenant does not narrow the rows.",
        "operationId": "get_api_persons_person_id_app_identities",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/persons/{person_id}/devices": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List devices",
        "description": "Lists every device linked to one person from device.person_link - link_id, device_uuid, person_id, first_used_at, last_used_at and status - returned under data.links. Edge cases: an unknown person_id returns 200 with links: [] rather than 404; person_id is a UUID column, so a non-UUID path segment fails at the database and surfaces as a 500 rather than a 400; revoked links are NOT filtered out - every row is returned regardless of its status value, so callers must filter on status themselves; the result set is unpaginated and unordered, and the query filters on person_id alone with no tenant or device-status predicate, so the caller's JWT tenant does not narrow the rows.",
        "operationId": "get_api_persons_person_id_devices",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "device_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/plan/{id}": {
      "get": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Read a plan produced by POST /intent/plan",
        "description": "Reads a single intent plan by plan_id from semantic.intent_plan, returning 200 {success:true, data:{plan_id, intent_id, subject_id, steps[], generated_by_agent_run_id, generated_at, status}}. QA edge cases: :id must be a well-formed UUID — getPlan runs a bare `WHERE plan_id = $1` with no try/catch, so a non-UUID path segment ('abc', '123') raises a Postgres 'invalid input syntax for type uuid' error that escapes as Fastify's default 500, NOT a 404; only a syntactically valid but unknown UUID returns the clean 404 {success:false, error:'plan <id> not found'}. The read is idempotent, unpaginated (the full steps array is inlined however long the plan is) and returns the CURRENT status, so fetching after POST /plan/:id/status is the way to confirm a lifecycle transition. There is no tenant filter in the query — any authenticated caller who knows a plan_id can read it — so tenant-isolation tests must not expect a 403/404 for another tenant's plan. /plan/:id is not on the gateway public allowlist, so the default-deny auth gate requires a valid tenant JWT before the handler runs.",
        "operationId": "get_plan_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — plan <id> not found"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/plan/{id}/status": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Advance a freshly-proposed plan to approved",
        "description": "Advances an intent plan through its lifecycle (proposed → approved → executing → completed|abandoned, plus proposed → abandoned) by UPDATEing semantic.intent_plan.status, returning 200 {success:true, data:{plan_id, status}}; reaching 'completed' or 'abandoned' also emits a semantic.plan.executed.v1 audit entry. QA edge cases: the handler does NOT enforce the transition graph — the UPDATE is unconditional, so jumping proposed → completed, or moving backwards from executing → proposed, both succeed with 200. Re-posting the same status is idempotent (row updates to the same value, still 200). Only presence of `status` is validated (400 when missing, empty string, or an empty body); everything after that is wrapped in a single catch that maps EVERY error to 404, so a status outside the DB enum ('shipped'), a non-UUID :id ('abc' → invalid input syntax for type uuid), and a valid-but-unknown plan_id all return 404 {success:false, error:...} — never 400 or 500 for those cases. The distinguishing signal is the `error` string, not the status code. The UPDATE has no tenant predicate, so any authenticated caller holding a plan_id can transition it; the path is not on the gateway public allowlist, so a valid tenant JWT is required.",
        "operationId": "post_plan_id_status",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "statu_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "approved",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — status required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — [sdk-semantic] plan '<id>' not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "status": "approved"
              }
            }
          }
        }
      }
    },
    "/api/policies": {
      "post": {
        "tags": [
          "sdk-policy"
        ],
        "summary": "Create a doctor-care-team policy bundle",
        "description": "Creates a versioned policy bundle (name + version + IQL source, with optional obligations) per FR-POL-4 and returns 201. Requires a valid tenant JWT (requireAuth). Edge cases: name, version and iql_source are all mandatory presence checks; obligations, when present, must be an object and each sub-field is type-checked - mask_fields must be an array of strings, row_filter must be an object, audit_level must be one of the allowed levels, and ttl_seconds must be a non-negative number (0 is allowed, -1 is a 400); a syntactically invalid IQL body is a 400 IQLParseError raised by the parser rather than the validator, so it is a distinct failure mode from ValidationError; re-creating the same name+version pair trips the unique constraint and is a 409 Conflict, so the endpoint is not idempotent - bump the version to publish a change. SCOPING: an optional app_id scopes the rule to ONE app; omit it for a tenant-wide rule that every app of the tenant is evaluated against (the pre-2026-08 behaviour, and what existing rows still are). tenant_id NULL with app_id NULL is a platform default. Uniqueness is enforced by two PARTIAL indexes - (tenant_id, name, version) WHERE app_id IS NULL and (tenant_id, app_id, name, version) WHERE app_id IS NOT NULL - so an app-scoped override may carry the SAME name and version as the tenant-wide rule it narrows; a single four-column UNIQUE would not have worked because Postgres treats NULLs as distinct and would have permitted unlimited duplicate tenant-wide rows.",
        "operationId": "post_api_policies",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "version": "1.0.0",
                    "iql_source": "subject.persona(role=\"doctor\") and relationship(type=\"care-team\")",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "obligations": {
                      "mask_fields": [
                        "ssn",
                        "dob"
                      ],
                      "row_filter": {
                        "care_team": true
                      },
                      "audit_level": "standard",
                      "ttl_seconds": 300
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — name is required / iql_source is required / version is required / obligations must be an object / obligations.mask_fields must be an array of strings / obligations.row_filter must be an object / obligations.audit_level must be one of <list> / obligations.ttl_seconds must be a non-negative number"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — policy with this name+version already exists"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "iql_source": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "obligations": {
                    "type": "object",
                    "properties": {
                      "mask_fields": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "row_filter": {
                        "type": "object",
                        "properties": {
                          "care_team": {
                            "type": "boolean"
                          }
                        }
                      },
                      "audit_level": {
                        "type": "string"
                      },
                      "ttl_seconds": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "<generated name>",
                "version": "1.0.0",
                "iql_source": "subject.persona(role=\"doctor\") and relationship(type=\"care-team\")",
                "tenant_id": "<id from auth>",
                "obligations": {
                  "mask_fields": [
                    "ssn",
                    "dob"
                  ],
                  "row_filter": {
                    "care_team": true
                  },
                  "audit_level": "standard",
                  "ttl_seconds": 300
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-policy"
        ],
        "summary": "List the tenant-wide policies for the calling tenant",
        "description": "Lists the ACTIVE policy bundles governing a scope, returned under data.policies. The tenant is taken from the caller's JWT and never from the query string - a policy names what a caller may do, so allowing one to be listed by naming somebody else's tenant would hand over their access model; a credential carrying no tenant answers 400 ValidationError. With no app_id the response is the tenant-wide rules only (app_id IS NULL). With ?app_id= it is that app's own rules FOLLOWED BY the tenant-wide rules the app inherits, ordered app-specific first. Edge cases: the ordering is for presentation and for callers that want the narrowest rule - it is NOT first-match-wins, because access rules COMPOSE and silently dropping an inherited tenant-wide rule because a more specific app rule exists would widen access at exactly the moment somebody added a restriction; only status='active' rows are returned, so draft and retired bundles are invisible here; an app_id that matches no policy is not an error and yields just the inherited tenant-wide rules.",
        "operationId": "get_api_policies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — This credential carries no tenant context"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/policies/evaluate": {
      "post": {
        "tags": [
          "sdk-policy"
        ],
        "summary": "Evaluate the doctor-care-team policy for a subject",
        "description": "Evaluates a stored policy against a subject and context per FR-POL-1 and returns the decision plus any obligations. Requires a valid tenant JWT (requireAuth). Edge cases: policy_id and subject_id are mandatory presence checks; purpose is conditionally required - it must be supplied whenever purpose_bound is true, and omitting it then is a 400 even though purpose is otherwise optional; a well-formed policy_id that does not resolve is a 404 NotFound raised from the service (matched on the message containing \"not found\"), not a 400; a policy that evaluates to deny is still a 200 with a deny decision in the body, so a deny must not be treated as an error status; a non-UUID policy_id or subject_id fails the uuid cast and surfaces as a 500 rather than a 400.",
        "operationId": "post_api_policies_evaluate",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_id": "{{var:target_id}}",
                    "context": {
                      "subject": {
                        "persona": {
                          "role": "doctor"
                        }
                      },
                      "rebac": {
                        "care-team:*": true
                      },
                      "projection_version": 1
                    },
                    "purpose": "treatment",
                    "purpose_bound": false,
                    "consent_receipts": [],
                    "resource_class": "sensitive",
                    "evaluate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — policy_id is required / subject_id is required / purpose is required when purpose_bound is true"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <service message containing \"not found\">"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "policy_id": {
                    "type": "string"
                  },
                  "subject_id": {
                    "type": "string"
                  },
                  "target_id": {
                    "type": "string"
                  },
                  "context": {
                    "type": "object",
                    "properties": {
                      "subject": {
                        "type": "object",
                        "properties": {
                          "persona": {
                            "type": "object",
                            "properties": {
                              "role": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      },
                      "rebac": {
                        "type": "object",
                        "properties": {
                          "care-team:*": {
                            "type": "boolean"
                          }
                        }
                      },
                      "projection_version": {
                        "type": "integer"
                      }
                    }
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "purpose_bound": {
                    "type": "boolean"
                  },
                  "consent_receipts": {
                    "type": "array",
                    "items": {}
                  },
                  "resource_class": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "policy_id": "<id from policies>",
                "subject_id": "<id from auth>",
                "target_id": "{{var:target_id}}",
                "context": {
                  "subject": {
                    "persona": {
                      "role": "doctor"
                    }
                  },
                  "rebac": {
                    "care-team:*": true
                  },
                  "projection_version": 1
                },
                "purpose": "treatment",
                "purpose_bound": false,
                "consent_receipts": [],
                "resource_class": "sensitive"
              }
            }
          }
        }
      }
    },
    "/api/policies/{policy_id}": {
      "get": {
        "tags": [
          "sdk-policy"
        ],
        "summary": "Fetch the freshly-created policy",
        "description": "Reads one policy bundle by its policy_id path param and returns it under data.policy. Requires a valid tenant JWT (requireAuth). Edge cases: the lookup is by id alone - the caller JWT tenant is never compared to the policy, so any authenticated caller can read any policy bundle including its IQL source; a well-formed but unknown policy_id returns 404 NotFound with the id echoed in details[], while a malformed non-UUID id fails the Postgres uuid cast inside the try/catch and is reported as a 500 InternalError rather than a 404 or 400; the response returns exactly the version row the id addresses - there is no \"latest version\" resolution here.",
        "operationId": "get_api_policies_policy_id",
        "parameters": [
          {
            "name": "policy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "policy_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No policy <policy_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/policy/{id}/evaluate": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Evaluate an active semantic policy (happy path)",
        "description": "Evaluates the semantic-service policy named by the :id path param against an evaluation context (subject_type, action, resource_type, trace_id). Mounted into the api-gateway from services/semantic-service, so the gateway default-deny authGate applies and a valid tenant JWT is required even though the handler declares no requireAuth. Edge cases: all four of subject_type, action, resource_type and trace_id are mandatory and any omission produces a single 400; a deny decision is still a 200 with the decision in the body, so only transport failures produce a non-200; note the catch is unconditional and maps EVERY service throw to 404, so an unknown policy id, a non-UUID id failing the uuid cast, an IQL evaluation error and a DB outage all return the same 404 with the raw message - a 500 is never returned from this route, which makes an infrastructure failure indistinguishable from a missing policy.",
        "operationId": "post_policy_id_evaluate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "subject_type": "Doctor",
                    "subject_id": "{{var:subject_id}}",
                    "action": "write",
                    "resource_type": "Prescription",
                    "resource_id": "{{var:resource_id}}",
                    "active_edges": [
                      {
                        "kind": "care-team",
                        "from_object_type": "Doctor",
                        "to_object_type": "Patient",
                        "to_object_id": "{{var:resource_id}}",
                        "active": true
                      }
                    ],
                    "trace_id": "{{var:trace_id}}",
                    "evaluate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — subject_type, action, resource_type, trace_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "EvaluateFailed — <service error message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject_type": {
                    "type": "string"
                  },
                  "subject_id": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string"
                  },
                  "resource_type": {
                    "type": "string"
                  },
                  "resource_id": {
                    "type": "string"
                  },
                  "active_edges": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string"
                        },
                        "from_object_type": {
                          "type": "string"
                        },
                        "to_object_type": {
                          "type": "string"
                        },
                        "to_object_id": {
                          "type": "string"
                        },
                        "active": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "trace_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "subject_type": "Doctor",
                "subject_id": "{{var:subject_id}}",
                "action": "write",
                "resource_type": "Prescription",
                "resource_id": "{{var:resource_id}}",
                "active_edges": [
                  {
                    "kind": "care-team",
                    "from_object_type": "Doctor",
                    "to_object_type": "Patient",
                    "to_object_id": "{{var:resource_id}}",
                    "active": true
                  }
                ],
                "trace_id": "{{var:trace_id}}"
              }
            }
          }
        }
      }
    },
    "/policy": {
      "get": {
        "tags": [
          "semantic-service"
        ],
        "summary": "List registered semantic policies",
        "description": "Lists semantic-service policies, optionally filtered by the tenant_id and ontology_id query params. Mounted into the api-gateway from services/semantic-service, so the gateway default-deny authGate applies and a valid tenant JWT is required even though the handler declares no requireAuth. Edge cases: tenant_id is optional and defaults to null when omitted, which lists the global/unscoped policies rather than everything the caller can see - and because the filter comes from the query string and not the JWT, passing another tenant id is not rejected, so tenant scoping must be tested explicitly; no matches returns 200 with an empty data array, never a 404; there is no limit/offset paging, so the whole result set comes back in one payload; the handler has no try/catch and no validation at all, so a non-UUID tenant_id or ontology_id fails the uuid cast and is surfaced by the Fastify default error handler as a 500.",
        "operationId": "get_policy",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalServerError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/policy/register": {
      "post": {
        "tags": [
          "semantic-service"
        ],
        "summary": "Register + activate a Doctor→Prescription semantic policy",
        "description": "Registers a semantic-service policy against an ontology from IQL source, optionally activating it immediately via the activate flag (default false, so a newly registered policy is inert until activated). Mounted into the api-gateway from services/semantic-service, so the gateway default-deny authGate applies and a valid tenant JWT is required even though the handler has no requireAuth of its own. Edge cases: ontology_id, name and iql_source are presence-checked; tenant_id is optional and defaults to null, which registers a global cross-tenant policy - omitting it is silently accepted rather than rejected, so tenant scoping must be asserted deliberately; an unknown ontology_id, an IQL parse failure and a duplicate name all collapse into the same 400 with the raw service message, so this route never returns 404 or 409; there is no idempotency key, so re-registering the same name either duplicates or conflicts depending on the schema and either way surfaces as a 400.",
        "operationId": "post_policy_register",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "register_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ontology_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "description": "Doctor with active care-team may write a Prescription",
                    "iql_source": "ALLOW Doctor WITH care-team(Patient) TO write Prescription",
                    "activate": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — ontology_id, name, iql_source required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "ontology_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "iql_source": {
                    "type": "string"
                  },
                  "activate": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "ontology_id": "<id from ontology>",
                "name": "<generated name>",
                "description": "Doctor with active care-team may write a Prescription",
                "iql_source": "ALLOW Doctor WITH care-team(Patient) TO write Prescription",
                "activate": true
              }
            }
          }
        }
      }
    },
    "/api/profile/bands/{app_identity_id}/{band_kind}": {
      "get": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "Read profile band for app_identity",
        "description": "Reads one L2 profile band (the encrypted fields_envelope) for an app identity and band kind, returning 200 with { data: { band } }. band_kind is validated against the closed set profile | preference | notification_routing before the query runs. Edge cases: an unrecognised band_kind is a 400 (not a 404); a valid band_kind for an app_identity_id that has never had that band upserted is a 404; a non-UUID app_identity_id reaches the Postgres UUID cast unguarded and surfaces as a 500; the band is keyed only by (app_identity_id, band_kind) so the handler performs no tenant check against the JWT; requires a valid JWT.",
        "operationId": "get_api_profile_bands_app_identity_id_band_kind",
        "parameters": [
          {
            "name": "app_identity_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "app_identity_id path parameter"
          },
          {
            "name": "band_kind",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "band_kind path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "band_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid band_kind"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/profile/bands": {
      "put": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "Upsert profile band",
        "description": "Upserts an L2 profile band: inserts a new profile.band_l2 row or, on conflict with an existing (app_identity_id, band_kind), replaces its fields_envelope and bumps updated_at. Returns 200 in both cases, so the call is fully idempotent and repeat PUTs never conflict. app_identity_id, band_kind and tenant_id are all mandatory; fields_envelope is optional and defaults to an empty object, which effectively clears the band. Edge cases: unrecognised band_kind is a 400; an empty-string tenant_id is treated as missing; a non-existent app_identity_id or tenant_id trips a foreign-key violation that escapes as an unhandled 500; requires a valid JWT.",
        "operationId": "put_api_profile_bands",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "band_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_identity_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "band_kind": "profile",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "fields_envelope": {
                      "display_name": "base64envelope=="
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — insert or update on table \"band_l2\" violates foreign key constraint"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app_identity_id": {
                    "type": "string"
                  },
                  "band_kind": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "fields_envelope": {
                    "type": "object",
                    "properties": {
                      "display_name": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "app_identity_id": "<id from app-identities>",
                "band_kind": "profile",
                "tenant_id": "<id from auth>",
                "fields_envelope": {
                  "display_name": "base64envelope=="
                }
              }
            }
          }
        }
      }
    },
    "/api/profile/secure-data/{person_id}": {
      "get": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "Read secure data",
        "description": "Reads the secure-data record for a person - the per-field encrypted envelopes plus the per-field state map (active / shredded) - returning 200 with { data: { secure_data } }. The row is created lazily by set-field, so a person who has never had a secure field written has no row at all. Edge cases: a person with no secure_data row is a 404; a person whose only fields have been shredded still returns 200, with the envelopes removed but the field_states entries retained as shredded (shredding never deletes the row); a non-UUID person_id hits the Postgres UUID cast unguarded and surfaces as a 500; no tenant scoping is applied; requires a valid JWT.",
        "operationId": "get_api_profile_secure_data_person_id",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "secure_data_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/profile/secure-data/{person_id}/shred-history": {
      "get": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "List shred history",
        "description": "Returns the append-only per-field shred history for a person as { data: { history } } with status 200. Read-only and unpaginated - the full log is returned, so a person with a long shred history returns every row. Edge cases: an unknown person_id (or one that has never been shredded) returns 200 with an empty history array rather than a 404; the same field shredded repeatedly yields one entry per shred call; a non-UUID person_id reaches the Postgres UUID cast unguarded and surfaces as a 500; no tenant scoping is applied; requires a valid JWT.",
        "operationId": "get_api_profile_secure_data_person_id_shred_history",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "shred_history_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/profile/secure-data/set-field": {
      "post": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "Set PAN field envelope",
        "description": "Sets or replaces one named encrypted field on a person's secure-data record, creating the row on first use and merging the field into the existing field_envelopes / field_states JSONB on subsequent calls. Returns 200 with the full updated record. Idempotent: re-posting the same field overwrites in place and the field state is (re-)set to active - which means a previously shredded field can be resurrected by writing it again. Edge cases: person_id, field_name and envelope are all mandatory and an empty string counts as missing; envelope is stored verbatim with no format or size validation; a person_id that does not exist trips a foreign-key violation that escapes as an unhandled 500; requires a valid JWT.",
        "operationId": "post_api_profile_secure_data_set_field",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "set_field_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "field_name": "pan",
                    "envelope": "cGFuLXNlY3JldA==",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — insert or update on table \"secure_data\" violates foreign key constraint"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "field_name": {
                    "type": "string"
                  },
                  "envelope": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "field_name": "pan",
                "envelope": "cGFuLXNlY3JldA=="
              }
            }
          }
        }
      }
    },
    "/api/profile/secure-data/shred-field": {
      "post": {
        "tags": [
          "sdk-profile"
        ],
        "summary": "Shred PAN field",
        "description": "Cryptographically shreds one named field on a person's secure-data record: the envelope bytes are removed from field_envelopes, field_states is stamped { state: \"shredded\", shredded_at }, and an append-only shred-log row is written. Returns 200 with the log entry. reason must be one of retention-expiry | dsar-erasure | operator-request. Edge cases: the UPDATE is unconditional, so shredding a person or field that does not exist still returns 200 and still writes a log row (no 404 path); repeat shreds of the same field are idempotent but each appends another log entry; audit_entry_id is optional and, when omitted, is generated from the emitted profile.field.shredded.v1 event; requires a valid JWT.",
        "operationId": "post_api_profile_secure_data_shred_field",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "shred_field_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "field_name": "pan",
                    "reason": "dsar-erasure",
                    "audit_entry_id": "{{var:audit_entry_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — invalid input syntax for type uuid"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "field_name": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "audit_entry_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from auth>",
                "field_name": "pan",
                "reason": "dsar-erasure",
                "audit_entry_id": "{{var:audit_entry_id}}"
              }
            }
          }
        }
      }
    },
    "/api/relationships/check": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Check care-team reachability with default budget",
        "description": "Evaluates whether a subject_persona_id can reach a target_persona_id via a relationship of the given kind, running a bounded graph traversal and returning a decision plus budget usage. Requires subject_persona_id, target_persona_id and kind (all trimmed). Optional budget object, if present, MUST have numeric depth_cap and visit_cap. All validation failures collect into one 400 whose details[] lists every failing field; a traversal/DB failure returns 500.",
        "operationId": "post_api_relationships_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "target_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "care-team",
                    "budget": {
                      "depth_cap": 4,
                      "visit_cap": 1024
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — details[] lists failing fields: subject_persona_id/target_persona_id/kind required, or budget must have numeric depth_cap and visit_cap, or body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject_persona_id": {
                    "type": "string"
                  },
                  "target_persona_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "budget": {
                    "type": "object",
                    "properties": {
                      "depth_cap": {
                        "type": "integer"
                      },
                      "visit_cap": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "subject_persona_id": "<id from personas>",
                "target_persona_id": "<id from personas>",
                "kind": "care-team",
                "budget": {
                  "depth_cap": 4,
                  "visit_cap": 1024
                }
              }
            }
          }
        }
      }
    },
    "/api/relationships": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Create a care-team relationship",
        "description": "Creates a new relationship edge between two personas, returning 201 with the created relationship. Requires non-empty kind, persona_a and persona_b (trimmed), and persona_a must differ from persona_b (self-edges rejected). Optional fields (scope, consent_ref, expires_at, reattest_due_at, cross_tenant) are coerced/ignored if wrong type. A DB check-constraint violation maps to 400; other insert failures return 500. NOTE on persona_b: the endpoint needs TWO DISTINCT personas, but the dependency graph is one node per METHOD+ENDPOINT so it cannot call POST /api/personas twice; persona_a therefore comes from the real producer and persona_b uses the seeded {{var:persona_id}}. That is safe here because rebac.relationship has NO foreign keys on persona_a/persona_b (verified) — they are loose UUIDs and the only rule enforced is that the two differ. Using the same producer for both was a real defect: it always resolved to one id and the request 400'd on every run.",
        "operationId": "post_api_relationships",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "relationship_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "care-team",
                    "persona_a": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_b": "{{var:persona_id}}",
                    "scope": {
                      "encounter_kind": "primary-care"
                    },
                    "consent_ref": "{{var:customer_consent_ref}}",
                    "expires_at": "2027-01-01T00:00:00Z",
                    "reattest_due_at": "2026-12-01T00:00:00Z",
                    "cross_tenant": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — details[] lists failing fields: kind/persona_a/persona_b required, persona_a and persona_b must differ, or body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "persona_a": {
                    "type": "string"
                  },
                  "persona_b": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "object",
                    "properties": {
                      "encounter_kind": {
                        "type": "string"
                      }
                    }
                  },
                  "consent_ref": {
                    "type": "string"
                  },
                  "expires_at": {
                    "type": "string"
                  },
                  "reattest_due_at": {
                    "type": "string"
                  },
                  "cross_tenant": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "kind": "care-team",
                "persona_a": "<id from personas>",
                "persona_b": "{{var:persona_id}}",
                "scope": {
                  "encounter_kind": "primary-care"
                },
                "consent_ref": "{{var:customer_consent_ref}}",
                "expires_at": "2027-01-01T00:00:00Z",
                "reattest_due_at": "2026-12-01T00:00:00Z",
                "cross_tenant": false
              }
            }
          }
        }
      }
    },
    "/api/relationships/{relationship_id}/scope": {
      "put": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Suspend the relationship and narrow scope",
        "description": "Updates a relationship edge's scope object and/or lifecycle status (FR-REB-4), returning the full updated relationship; setting status='terminated' also stamps terminated_at and invalidates the cached decisions for both personas. Body must be an object carrying at least one of scope or status — an empty body is a 400, and status is constrained to open|active|suspended|terminated|expired. Edge cases: an unknown or non-existent relationship_id yields 404 (the UPDATE returns zero rows); a scope-only update replaces the jsonb scope wholesale rather than merging; re-terminating an already-terminated edge succeeds idempotently and simply re-stamps terminated_at; a non-UUID relationship_id makes Postgres reject the cast and surfaces as 500. Requires a valid tenant JWT (route preHandler requireAuth plus the gateway default-deny gate).",
        "operationId": "put_api_relationships_relationship_id_scope",
        "parameters": [
          {
            "name": "relationship_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "relationship_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "scope_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "suspended",
                    "scope": {
                      "encounter_kind": "primary-care"
                    },
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Relationship <relationship_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "object",
                    "properties": {
                      "encounter_kind": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "status": "suspended",
                "scope": {
                  "encounter_kind": "primary-care"
                }
              }
            }
          }
        }
      }
    },
    "/api/resellers": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Create a reseller with brand + commission rules",
        "description": "Creates a first-class reseller (white-label brand + commission config) in tenant.reseller and emits reseller.created.v1 to the admin audit pool. Requires org_id and brand_name; invoice_aggregation defaults to 'per-tenant' and cname_host, support_contact and commission_rules are optional (the last two default to {} when omitted or non-object). QA edge cases: org_id must reference an existing tenant.org row — a valid-but-unknown UUID trips the FK and is reported as 400 ValidationError, while a non-UUID string produces a 500 because uncaught() only text-matches 'violates foreign key' / 'duplicate key' / 'not found'. There is NO unique constraint on brand_name or cname_host, so repeat POSTs create additional reseller_id rows (non-idempotent; duplicate brands and colliding CNAME hosts are accepted). support_contact and commission_rules are stored as opaque JSONB with no schema check — bad emails, negative or >100 commission percentages all persist. invoice_aggregation is double-guarded: the validator rejects out-of-enum values with 400, and the column CHECK would otherwise reject them. The endpoint is not tenant-scoped or admin-gated: any caller with a valid tenant JWT can create a reseller against any org_id.",
        "operationId": "post_api_resellers",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reseller_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "brand_name": "Acme QA Sample",
                    "cname_host": "reseller.example.com",
                    "support_contact": {
                      "email": "support@reseller.example.com",
                      "phone": "+1-555-0100"
                    },
                    "commission_rules": {
                      "default_pct": 15
                    },
                    "invoice_aggregation": "consolidated",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — duplicate key value violates unique constraint"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "org_id": {
                    "type": "string"
                  },
                  "brand_name": {
                    "type": "string"
                  },
                  "cname_host": {
                    "type": "string"
                  },
                  "support_contact": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string"
                      },
                      "phone": {
                        "type": "string"
                      }
                    }
                  },
                  "commission_rules": {
                    "type": "object",
                    "properties": {
                      "default_pct": {
                        "type": "integer"
                      }
                    }
                  },
                  "invoice_aggregation": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "org_id": "<id from auth>",
                "brand_name": "<generated name>",
                "cname_host": "reseller.example.com",
                "support_contact": {
                  "email": "support@reseller.example.com",
                  "phone": "+1-555-0100"
                },
                "commission_rules": {
                  "default_pct": 15
                },
                "invoice_aggregation": "consolidated"
              }
            }
          }
        }
      }
    },
    "/api/resolver/explain": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Explain primary_persona_id provenance",
        "description": "Returns provenance metadata (source SDK, computed_at, projection_version) for a single resolved identity attribute of a person within a tenant/app context. Requires person_id, app_id, tenant_id and attribute — any missing field yields 400. attribute is NOT enum-validated: an unrecognized attribute returns 200 with source_sdk=\"sdk-identity\" via the explain() default branch. A downstream resolve failure surfaces as Fastify default 500 (no explicit branch).",
        "operationId": "post_api_resolver_explain",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "explain_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "attribute": "primary_persona_id",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "attribute": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "person_id": "<id from scim>",
                "app_id": "<id from auth>",
                "tenant_id": "<id from auth>",
                "attribute": "primary_persona_id"
              }
            }
          }
        }
      }
    },
    "/api/resolver/resolve": {
      "post": {
        "tags": [
          "sdk-identity-resolver"
        ],
        "summary": "Resolve identity context",
        "description": "Resolves and returns the full IdentityContext for a person in a tenant/app (personas, role closure, consents, pool indices) from the Redis/Postgres projection with a live-compose fallback on projection miss. Requires person_id, app_id and tenant_id; optional bypass_cache boolean skips the projection cache. Any missing required field yields 400; an internal resolve failure falls through to Fastify default 500.",
        "operationId": "post_api_resolver_resolve",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "person_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "bypass_cache": false,
                    "resolve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing required fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "person_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "tenant_id": {
                    "type": "string"
                  },
                  "bypass_cache": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "person_id": "<id from scim>",
                "app_id": "<id from auth>",
                "tenant_id": "<id from auth>",
                "bypass_cache": false
              }
            }
          }
        }
      }
    },
    "/api/role-assignments/{assignment_id}/revoke": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Revoke role assignment",
        "description": "Revokes an active role assignment by stamping persona.role_assignment.revoked_at = now() and emitting identity.role.revoked.v1; returns 200 with the revoked assignment record. The UPDATE is guarded by `revoked_at IS NULL`, so it only affects assignments that are still active. Edge cases: revoking is NOT idempotent from the caller's view — a second revoke of the same assignment matches zero rows and returns 404, as does an assignment_id that never existed; a non-UUID assignment_id is rejected by Postgres and surfaces as an uncaught 500; there is no tenant/persona ownership check in this handler beyond the JWT gate, so scoping relies on the caller holding the assignment_id. Requires a valid tenant JWT.",
        "operationId": "post_api_role_assignments_assignment_id_revoke",
        "parameters": [
          {
            "name": "assignment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "assignment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "Internal Server Error — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/role-assignments": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Assign role to persona",
        "description": "Assigns a role template to an L4 persona, inserting a persona.role_assignment row and emitting the identity.role.assigned.v1 audit event; returns 201 with the created assignment (assignment_id, persona_id, role_template_id, assigned_at, revoked_at=null, assigned_by). Only persona_id and role_template_id are validated as present — assigned_by is optional and stored as NULL when omitted. Edge cases: the endpoint is NOT idempotent, so repeating the same body creates a second active assignment for the same persona/role pair; a persona_id or role_template_id that does not exist (or is not a UUID) is not pre-checked and fails at the Postgres FK/cast layer as an uncaught 500; empty-string values are falsy and are rejected as missing fields. Requires a valid tenant JWT.",
        "operationId": "post_api_role_assignments",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "role_assignment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role_template_id": "{{var:role_template_id}}",
                    "assigned_by": "sdk-persona.qa-assign",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "persona_id": {
                    "type": "string"
                  },
                  "role_template_id": {
                    "type": "string"
                  },
                  "assigned_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "persona_id": "<id from personas>",
                "role_template_id": "{{var:role_template_id}}",
                "assigned_by": "sdk-persona.qa-assign"
              }
            }
          }
        }
      }
    },
    "/api/role-templates": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Create a tenant-scoped doctor role template",
        "description": "Creates a per-app role template in tenant.role_template with an optional inheritance parent and an opaque JSONB permissions map. Despite the 'sdk-rebac' label the route is served by sdk-tenant (packages/sdk-tenant/src/server/routes.ts -> createRoleTemplateHandler). Requires app_id and name; tenant_id omitted means a platform-default template (tenant_id NULL), tenant_id present means a tenant override. QA edge cases: uniqueness is enforced by two PARTIAL unique indexes — (app_id, name) WHERE tenant_id IS NULL and (tenant_id, app_id, name) WHERE tenant_id IS NOT NULL — so re-POSTing the same triple returns 409 Conflict, while the same (app_id, name) is legitimately allowed once globally and once per tenant. app_id is a TEXT FK to tenant.app and tenant_id/parent_role_template_id are UUID FKs; an unknown value trips the FK and is reported as 400 ValidationError (never 404), whereas a malformed UUID for tenant_id or parent_role_template_id yields 500 because uncaught() only text-matches FK/duplicate/not-found. permissions is stored verbatim with no schema validation (unknown keys, non-boolean values and arbitrary nesting all persist); a non-object permissions value is silently coerced to {} rather than rejected. No cycle check exists on parent_role_template_id. Auth is a plain valid-JWT check — the caller's tenant claim is never compared to the body tenant_id, so cross-tenant template creation is not blocked at this layer.",
        "operationId": "post_api_role_templates",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "role_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "permissions": {
                      "chart.read": true,
                      "chart.write": false
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "Conflict — duplicate key value violates unique constraint \"role_template_tenant_uniq\""
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "app_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "permissions": {
                    "type": "object",
                    "properties": {
                      "chart.read": {
                        "type": "boolean"
                      },
                      "chart.write": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "app_id": "<id from auth>",
                "name": "<generated name>",
                "permissions": {
                  "chart.read": true,
                  "chart.write": false
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "List role templates for an app",
        "description": "Lists role templates for one app. tenant.role_template is keyed (tenant_id, app_id, name) with a NULLABLE tenant_id, and that nullability carries the whole model: a NULL row is the PLATFORM DEFAULT shipped with the app (unique on app_id+name) and a tenant_id row is that tenant OVERRIDING the same role name (unique on tenant_id+app_id+name). A tenant can therefore redefine what Manager means without forking the app, and a tenant that never touches it keeps inheriting. The response must let a client tell the two apart WITHOUT comparing tenant_id itself — the roles screen renders inherited rows read-only and offers override as a deliberate action, and if it had to infer origin client-side the two would eventually be confused and every tenant would end up owning a private copy of every default role, which is exactly the fork the nullable column exists to avoid. parent_role_template_id is returned so the inheritance chain can be shown and a permission traced to the ancestor that grants it.",
        "operationId": "get_api_role_templates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "role_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — app_id is required"
          },
          "401": {
            "description": "Unauthorized — authentication required"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/saml/{tenant_id}/metadata": {
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Metadata XML for a known tenant",
        "description": "Public SAML SP metadata XML for a tenant (FR-IDN-8). No auth; buildSamlSpMetadata() renders the SP EntityDescriptor from the request host + tenant_id and replies application/samlmetadata+xml with 200.",
        "operationId": "get_saml_tenant_id_metadata",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "metadata_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": []
      }
    },
    "/saml/{tenant_id}/acs": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Mock-adapter assertion JIT-provisions a person",
        "description": "SAML Assertion Consumer Service (FR-IDN-8). The route has no auth preHandler (the IdP POSTs directly). With the default SAML_ADAPTER=mock the body is treated as an already-parsed assertion { name_id, email, groups, attributes }; downstream consumeSamlAssertion() finds-or-JIT-provisions identity.person, merges saml_nameid+email aliases, upserts identity.app_identity, and returns person_id/app_identity_id/alias_ids/jit_provisioned/role_template_id.",
        "operationId": "post_saml_tenant_id_acs",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "acs_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name_id": "qa.user@example.com",
                    "email": "qa.user@example.com",
                    "groups": [
                      "Engineering"
                    ],
                    "attributes": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — SAML_ADAPTER=<kind> but <package> is not installed. Run: pnpm --filter @projexlight/sdk-identity add <package>"
          },
          "401": {
            "description": "SamlSignatureFailed — <signature/cert verification error from the adapter>"
          },
          "404": {
            "description": "NotFound — <not found error from consumeSamlAssertion>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name_id": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  },
                  "groups": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "attributes": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "name_id": "<generated email>",
                "email": "<generated email>",
                "groups": [
                  "Engineering"
                ],
                "attributes": {}
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/calendar-push": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Push appointment to Google",
        "description": "Push a single appointment to a connection's external calendar, creating the external event or updating/cancelling it if already mapped (this is how reschedule/cancel propagate to the provider). Records the appointment <-> external event mapping. tenant_id and connection_id required.",
        "operationId": "post_api_scheduling_appointments_appointment_id_calendar_push",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "calendar_push_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "connection_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and connection_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — connection/appointment not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "connection_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "connection_id": "<id from scheduling>"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/cancel": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Cancel the appointment",
        "description": "Cancel an appointment (idempotent - re-cancelling is a no-op), record the reason, bump the ICS SEQUENCE, and fire a cancellation notice (ICS METHOD:CANCEL) to both parties. Appends a booking_event.",
        "operationId": "post_api_scheduling_appointments_appointment_id_cancel",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "cancel_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "Customer requested cancellation",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason": "Customer requested cancellation"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/confirm": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Confirm the appointment",
        "description": "Confirm a pending/confirmed appointment (idempotent) and fire the confirmation notice to both parties (pluggable booking notifier -> sdk-notification). Sets confirmed_at and appends a booking_event. Confirming a cancelled/completed appointment returns 409.",
        "operationId": "post_api_scheduling_appointments_appointment_id_confirm",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "confirm_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          },
          "409": {
            "description": "InvalidTransition — cannot confirm an appointment in status 'cancelled'"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/events": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List booking events",
        "description": "Return the append-only booking lifecycle timeline for an appointment (created/confirmed/rescheduled/cancelled/notified), oldest first. tenant_id query param required.",
        "operationId": "get_api_scheduling_appointments_appointment_id_events",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/appointments/{appointment_id}/ics": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Download the ICS invite",
        "description": "Return the RFC 5545 iCalendar (.ics) invite for the appointment as text/calendar. STATUS/METHOD reflect the current state (CONFIRMED/REQUEST, or CANCELLED/CANCEL once cancelled); SEQUENCE advances across reschedules/cancels so calendar clients accept updates.",
        "operationId": "get_api_scheduling_appointments_appointment_id_ics",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ics_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/appointments/{appointment_id}/rebook": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Rebook 3h later",
        "description": "Rescue/rebook a (no-show or cancelled) appointment into a NEW confirmed appointment at a new window, cloning host/subject/meeting-type/title and linking back via rescheduled_from. Double-book prevention runs on the new window (409 on overlap; 400 if end<=start). Reminders are scheduled for the new slot. tenant_id, start_time, end_time required.",
        "operationId": "post_api_scheduling_appointments_appointment_id_rebook",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "rebook_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "start_time": "2026-01-15T10:30:00Z",
                    "end_time": "2026-01-15T10:30:00Z",
                    "timezone": "America/New_York",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, start_time and end_time are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          },
          "409": {
            "description": "DoubleBooking — the host already has an appointment overlapping this window"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string"
                  },
                  "end_time": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "start_time": "<generated futuredatetime+180m>",
                "end_time": "<generated futuredatetime+210m>",
                "timezone": "America/New_York"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/reminders": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List reminders",
        "description": "List an appointment's scheduled reminders (soonest first). tenant_id query param required.",
        "operationId": "get_api_scheduling_appointments_appointment_id_reminders",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reminder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Schedule 24h/2h/15m reminders",
        "description": "Schedule the pre-meeting reminder fan-out for an appointment (default 24h/2h/15m before, override via offsets_minutes). One reminder row per offset at start_time - offset; offsets already in the past are skipped; idempotent per (appointment, offset). tenant_id required.",
        "operationId": "post_api_scheduling_appointments_appointment_id_reminders",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reminder_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "offsets_minutes": [
                      1440,
                      120,
                      15
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "offsets_minutes": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "offsets_minutes": [
                  1440,
                  120,
                  15
                ]
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}/reschedule": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Reschedule 2h later",
        "description": "Move an appointment to a new window. Double-book prevention runs on the new window (excluding this appointment); ICS SEQUENCE is bumped so calendar clients accept the change; a reschedule notice fires. end_time must be after start_time (400). Cancelled/completed appointments cannot be moved (409).",
        "operationId": "post_api_scheduling_appointments_appointment_id_reschedule",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reschedule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "start_time": "2026-01-15T10:30:00Z",
                    "end_time": "2026-01-15T10:30:00Z",
                    "timezone": "America/New_York",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, start_time and end_time are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — appointment not found"
          },
          "409": {
            "description": "DoubleBooking — the host already has an appointment overlapping this window"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string"
                  },
                  "end_time": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "start_time": "<generated futuredatetime+120m>",
                "end_time": "<generated futuredatetime+150m>",
                "timezone": "America/New_York"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List appointments for a host",
        "description": "List a tenant's appointments, ordered by start_time, with optional host_persona_id / subject_persona_id / status / start_after / start_before filters. Only tenant_id is required.",
        "operationId": "get_api_scheduling_appointments",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "appointment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Book a 30-minute confirmed appointment",
        "description": "Book an appointment on a host's calendar with double-booking prevention: the overlap check and insert run in one transaction, so a window that overlaps an existing non-cancelled appointment for the same host is rejected with 409. end_time must be strictly after start_time (else 400). subject_persona_id is the invitee/lead persona. Required: tenant_id, host_persona_id, title, start_time, end_time.",
        "operationId": "post_api_scheduling_appointments",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "appointment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "host_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "meeting_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "title": "sample-text",
                    "description": "Discovery call booked from availability",
                    "start_time": "2026-01-15T10:30:00Z",
                    "end_time": "2026-01-15T10:30:00Z",
                    "timezone": "America/New_York",
                    "location_type": "video",
                    "location_detail": "Google Meet",
                    "meeting_url": "https://meet.example.com/abc",
                    "attendees": [],
                    "notes": "Booked via QA",
                    "entity_ref": "lead:{{dynamic:uuid}}",
                    "source": "internal",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, host_persona_id, title, start_time and end_time are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "DoubleBooking — the host already has an appointment overlapping this window"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "host_persona_id": {
                    "type": "string"
                  },
                  "subject_persona_id": {
                    "type": "string"
                  },
                  "meeting_type_id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "start_time": {
                    "type": "string"
                  },
                  "end_time": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "location_type": {
                    "type": "string"
                  },
                  "location_detail": {
                    "type": "string"
                  },
                  "meeting_url": {
                    "type": "string"
                  },
                  "attendees": {
                    "type": "array",
                    "items": {}
                  },
                  "notes": {
                    "type": "string"
                  },
                  "entity_ref": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "host_persona_id": "<id from personas>",
                "subject_persona_id": "<id from personas>",
                "meeting_type_id": "<id from scheduling>",
                "title": "<generated text>",
                "description": "Discovery call booked from availability",
                "start_time": "<generated futuredatetime>",
                "end_time": "<generated futuredatetime+30m>",
                "timezone": "America/New_York",
                "location_type": "video",
                "location_detail": "Google Meet",
                "meeting_url": "https://meet.example.com/abc",
                "attendees": [],
                "notes": "Booked via QA",
                "entity_ref": "lead:{{dynamic:uuid}}",
                "source": "internal"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/appointments/{appointment_id}": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Get a booked appointment by id",
        "description": "Fetch a single appointment by id (tenant-scoped). Returns 404 if no appointment with that id exists for the tenant. tenant_id query param is required.",
        "operationId": "get_api_scheduling_appointments_appointment_id",
        "parameters": [
          {
            "name": "appointment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "appointment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "appointment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "NotFound — appointment not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/availability": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Get availability for a host on a date for a meeting type",
        "description": "Compute a host's bookable time slots for a given ISO date, honoring the host's per-weekday business hours (in the rule's IANA timezone) and marking slots that overlap an existing non-cancelled appointment as unavailable. A weekday with no active rule returns zero slots (still HTTP 200). Slot length is the meeting type's duration when meeting_type_id is given, else slot_minutes, else the rule interval. Required query params: tenant_id, host_persona_id, date (YYYY-MM-DD).",
        "operationId": "get_api_scheduling_availability",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "availability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, host_persona_id and date query params are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/availability-rules": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Set Monday 09:00-17:00 America/New_York availability",
        "description": "Set (upsert) a host's bookable business hours for one weekday, in an IANA timezone. Idempotent — re-posting the same (tenant, host, weekday) overwrites the existing rule (one rule per weekday). A weekday with no active rule is treated as a closed day by slot generation. weekday is 0 (Sunday) through 6 (Saturday); start_time/end_time are wall-clock 'HH:MM' in the given timezone and end must be after start.",
        "operationId": "post_api_scheduling_availability_rules",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "availability_rule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "host_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "weekday": 1,
                    "start_time": "09:00",
                    "end_time": "17:00",
                    "timezone": "America/New_York",
                    "slot_interval_minutes": 30,
                    "is_active": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, host_persona_id and weekday are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "host_persona_id": {
                    "type": "string"
                  },
                  "weekday": {
                    "type": "integer"
                  },
                  "start_time": {
                    "type": "string"
                  },
                  "end_time": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "slot_interval_minutes": {
                    "type": "integer"
                  },
                  "is_active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "host_persona_id": "<id from personas>",
                "weekday": 1,
                "start_time": "09:00",
                "end_time": "17:00",
                "timezone": "America/New_York",
                "slot_interval_minutes": 30,
                "is_active": true
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List the availability rules for the host just configured",
        "description": "The recurring availability rules for ONE host, returned as data.rules. BOTH tenant_id AND host_persona_id are required query parameters - listAvailabilityRules takes the pair, so this route cannot list rules across hosts and omitting either is the same single 400. Returns the rules as authored (weekday plus start/end local times), NOT bookable slots: GET /api/scheduling/availability is the route that intersects these rules with existing appointments, meeting-type duration and time-off to produce concrete slots. Reading rules and computing slots client-side duplicates that logic and misses the exclusions. An unknown host_persona_id is 200 with an empty array rather than 404, because the handler never checks the persona exists.",
        "operationId": "get_api_scheduling_availability_rules",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and host_persona_id query params required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/calendar-connections/{connection_id}": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Get a connection by id",
        "description": "Fetch a single calendar connection by id (tenant-scoped). 404 if not found. tenant_id query param required.",
        "operationId": "get_api_scheduling_calendar_connections_connection_id",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "connection_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "calendar_connection_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — connection not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/scheduling/calendar-connections/{connection_id}/sync": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Sync the connection",
        "description": "Run a two-way sync for a connection: push unmapped non-cancelled appointments to the external calendar (outbound) and pull provider changes to apply reschedules/cancellations (inbound), advancing the sync token. Idempotent. Returns {pushed, pulled, applied}. tenant_id required.",
        "operationId": "post_api_scheduling_calendar_connections_connection_id_sync",
        "parameters": [
          {
            "name": "connection_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "connection_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "sync_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — connection not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/calendar-connections": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List connections",
        "description": "List a tenant's calendar connections (optionally filtered by host_persona_id), newest first. tenant_id query param required.",
        "operationId": "get_api_scheduling_calendar_connections",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "calendar_connection_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Connect a Google calendar",
        "description": "Bind a host to an external calendar provider (google/microsoft/caldav) via an sdk-connectors install. Upsert per (host, provider, external calendar). tenant_id, host_persona_id and provider are required. direction defaults to both.",
        "operationId": "post_api_scheduling_calendar_connections",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "calendar_connection_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "host_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "provider": "google",
                    "connector_install_id": null,
                    "external_calendar_id": "primary-{{dynamic:uuid}}",
                    "direction": "both",
                    "metadata": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, host_persona_id and provider are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "host_persona_id": {
                    "type": "string"
                  },
                  "provider": {
                    "type": "string"
                  },
                  "connector_install_id": {
                    "nullable": true
                  },
                  "external_calendar_id": {
                    "type": "string"
                  },
                  "direction": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "host_persona_id": "<id from personas>",
                "provider": "google",
                "connector_install_id": null,
                "external_calendar_id": "primary-{{dynamic:uuid}}",
                "direction": "both",
                "metadata": {}
              }
            }
          }
        }
      }
    },
    "/api/scheduling/meeting-types": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List meeting types for the tenant",
        "description": "List all meeting types for a tenant, active first then newest first. tenant_id query param is required. Returns the reusable meeting kinds a host can offer for booking.",
        "operationId": "get_api_scheduling_meeting_types",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "meeting_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Create a 30-minute discovery call meeting type",
        "description": "Create a reusable meeting type (bookable meeting kind) for a tenant — name, unique slug, duration (15/30/45/60 min are the common presets), booking buffers and location type. Slug is unique per tenant (a duplicate returns 409). Referenced by availability slotting, appointments and public scheduling links. tenant_id, name and slug are required.",
        "operationId": "post_api_scheduling_meeting_types",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "meeting_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "host_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "slug": "discovery-{{dynamic:uuid}}",
                    "description": "30-minute discovery call",
                    "duration_minutes": 30,
                    "buffer_before_minutes": 5,
                    "buffer_after_minutes": 5,
                    "location_type": "video",
                    "location_detail": "Google Meet",
                    "metadata": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, name and slug are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "Conflict — a meeting type with this slug already exists for the tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "host_persona_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "duration_minutes": {
                    "type": "integer"
                  },
                  "buffer_before_minutes": {
                    "type": "integer"
                  },
                  "buffer_after_minutes": {
                    "type": "integer"
                  },
                  "location_type": {
                    "type": "string"
                  },
                  "location_detail": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "host_persona_id": "<id from personas>",
                "name": "<generated name>",
                "slug": "discovery-{{dynamic:uuid}}",
                "description": "30-minute discovery call",
                "duration_minutes": 30,
                "buffer_before_minutes": 5,
                "buffer_after_minutes": 5,
                "location_type": "video",
                "location_detail": "Google Meet",
                "metadata": {}
              }
            }
          }
        }
      }
    },
    "/api/scheduling/no-show/scan": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Run a no-show scan",
        "description": "Mark confirmed appointments whose end_time passed by grace_minutes (default 10) and that were never completed as no_show, appending a booking_event for each. Returns {marked, appointment_ids} so a caller can offer a rescue/rebook. Also runs on the scheduling worker timer.",
        "operationId": "post_api_scheduling_no_show_scan",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "scan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "grace_minutes": 10,
                    "batch_size": 100,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grace_minutes": {
                    "type": "integer"
                  },
                  "batch_size": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "grace_minutes": 10,
                "batch_size": 100
              }
            }
          }
        }
      }
    },
    "/api/scheduling/public/appointments/{public_token}/cancel": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Invitee cancels their booking with their token",
        "description": "PUBLIC: the invitee cancels a booking they made through a shared link, freeing the host's slot, stamping cancelled_at with the optional reason, recording a booking_event and firing the cancellation notice. Like confirm, AUTHORISATION IS THE TOKEN — the path parameter is the high-entropy public_token issued at booking, never the appointment_id, because accepting an id would let anyone cancel a stranger's meeting by guessing UUIDs. Edge cases: an unknown or wrong token is a flat 404 revealing nothing; cancelling an already-cancelled appointment fails the lifecycle guard rather than silently succeeding; the freed window becomes bookable again immediately, since the double-book check ignores cancelled rows; reason is optional and is persisted to cancel_reason for the host's records.",
        "operationId": "post_api_scheduling_public_appointments_public_token_cancel",
        "parameters": [
          {
            "name": "public_token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "public_token path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "cancel_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "Something came up - will rebook next week",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "Something came up - will rebook next week"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/public/appointments/{public_token}/confirm": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Invitee confirms the pending booking with their token",
        "description": "PUBLIC: the invitee confirms a booking they made through a shared link, moving it pending -> confirmed, stamping confirmed_at, recording a booking_event and firing the confirmation notice. This is the double opt-in step: a public booking is created 'pending' because the invitee's email is unverified at booking time, so an unverified address never holds a confirmed slot on the host's calendar. AUTHORISATION IS THE TOKEN: the path parameter is the high-entropy public_token issued at booking, NOT the appointment_id — accepting a raw appointment_id here would be an IDOR (guess an id, confirm or cancel a stranger's meeting), so passing one returns 404. Edge cases: an unknown, wrong or already-used-then-cancelled token is a flat 404 that reveals nothing; confirming an already-confirmed appointment is idempotent (confirmed_at is preserved via COALESCE) rather than an error; confirming a cancelled or completed appointment fails the lifecycle guard.",
        "operationId": "post_api_scheduling_public_appointments_public_token_confirm",
        "parameters": [
          {
            "name": "public_token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "public_token path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "confirm_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/scheduling/public/links/{slug}/availability": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Anonymous visitor lists open slots for a date inside the window",
        "description": "PUBLIC: open slots on one date for a shared booking link, computed from the host's availability rules in their IANA timezone with buffers applied and existing appointments excluded. Slot length comes from the link's meeting type. No auth — the caller is an anonymous prospect — so the gateway allowlists /api/scheduling/public/. The requested date is clamped to the link's max_days_ahead window, so an anonymous caller cannot walk the host's calendar arbitrarily far into the future; a date far in the past is rejected too. The minimum-notice rule is NOT applied to the date probe itself (only to the actual booking), so today's page can still render today's later slots. Edge cases: 400 when the date query param is missing or outside the window; unknown/deactivated/expired slug is an indistinguishable 404; a date with no configured availability rule returns an empty slots array with total_available 0 rather than an error.",
        "operationId": "get_api_scheduling_public_links_slug_availability",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "slug path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "availability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — date query param required (YYYY-MM-DD)"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": []
      }
    },
    "/api/scheduling/public/links/{slug}/book": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Prospect books a slot through the public link",
        "description": "PUBLIC (anonymous) booking: a prospect opens a shared scheduling link and books without any tenant login, so this route is deliberately outside requireAuth and the gateway allowlists the /api/scheduling/public/ prefix. It is written for an untrusted caller. end_time is DERIVED from the link's meeting type when omitted, so a client cannot book a 6-hour slot against a 30-minute meeting type. The link's own guardrails are enforced SERVER-side: a start_time beyond max_days_ahead or inside min_notice_minutes is rejected 400 even though the UI also shows them. The appointment is created as 'pending', NOT confirmed — the invitee's email is unverified at booking time, so double opt-in is what stops an unverified address from holding a confirmed slot on the host's calendar; confirming via the returned token moves it pending -> confirmed. The response carries a high-entropy public_token which is the ONLY key the public confirm/cancel routes accept (acting on a raw appointment_id would be an IDOR). Edge cases: an unknown, deactivated or expired slug is an indistinguishable 404 — never a 403 that would confirm the slug exists; a slot taken between rendering and submitting returns 409 via the transactional double-book check; 400 when start_time, invitee_name or invitee_email is missing.",
        "operationId": "post_api_scheduling_public_links_slug_book",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "slug path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "book_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "start_time": "2026-01-15T10:30:00Z",
                    "end_time": "2026-01-15T10:30:00Z",
                    "invitee_name": "Acme QA Sample",
                    "invitee_email": "qa.user@example.com",
                    "timezone": "America/New_York",
                    "notes": "Booked from the website pricing page",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — start_time, invitee_name and invitee_email are required"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "DoubleBooking — that slot was just taken"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start_time": {
                    "type": "string"
                  },
                  "end_time": {
                    "type": "string"
                  },
                  "invitee_name": {
                    "type": "string"
                  },
                  "invitee_email": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "start_time": "<generated futuredatetime+48h>",
                "end_time": "<generated futuredatetime+49h>",
                "invitee_name": "<generated name>",
                "invitee_email": "<generated email>",
                "timezone": "America/New_York",
                "notes": "Booked from the website pricing page"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/public/links/{slug}": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Anonymous visitor loads the booking page for a live link",
        "description": "PUBLIC: resolve a shared booking slug into exactly what a booking page needs to render — title, description, the booking window (max_days_ahead / min_notice_minutes) and the meeting duration resolved from the link's meeting type. No auth: the visitor is an anonymous prospect, so the gateway allowlists /api/scheduling/public/. The projection is deliberately narrow — tenant_id, host_persona_id, link_id and meeting_type_id are NEVER returned, so a leaked slug cannot be used to enumerate a tenant's internals. Edge cases: an unknown slug, a deactivated link (is_active=false) and an expired link (expires_at in the past) ALL return an identical 404 rather than a 403, so an attacker cannot use the status code to discover which slugs exist; duration_minutes is null when the link pins no meeting type, in which case booking falls back to a 30-minute default.",
        "operationId": "get_api_scheduling_public_links_slug",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "slug path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "link_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": []
      }
    },
    "/api/scheduling/reminders/tick": {
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Run a reminder tick",
        "description": "Drain due reminders on demand: claim pending reminder rows whose remind_at has passed (FOR UPDATE SKIP LOCKED), fire the reminder notice for each still-active appointment, and mark rows sent/skipped. Also runs on a timer when SCHEDULING_WORKER_ENABLED. batch_size optional. Returns {claimed, sent, skipped}.",
        "operationId": "post_api_scheduling_reminders_tick",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tick_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "batch_size": 50,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "batch_size": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "batch_size": 50
              }
            }
          }
        }
      }
    },
    "/api/scheduling/scheduling-links": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "List scheduling links",
        "description": "List a tenant's public booking links (active first, newest first). tenant_id query param required.",
        "operationId": "get_api_scheduling_scheduling_links",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "scheduling_link_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Create a booking link",
        "description": "Create a shareable public booking link (Calendly-style) binding a host + meeting type to a globally-unique slug. tenant_id, host_persona_id and slug are required; a duplicate slug returns 409. max_days_ahead / min_notice_minutes bound the self-book window.",
        "operationId": "post_api_scheduling_scheduling_links",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "scheduling_link_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "host_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "meeting_type_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "book-{{dynamic:uuid}}",
                    "title": "Book a discovery call",
                    "description": "30-minute discovery call",
                    "max_days_ahead": 30,
                    "min_notice_minutes": 120,
                    "expires_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, host_persona_id and slug are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "409": {
            "description": "Conflict — a scheduling link with this slug already exists"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "host_persona_id": {
                    "type": "string"
                  },
                  "meeting_type_id": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "max_days_ahead": {
                    "type": "integer"
                  },
                  "min_notice_minutes": {
                    "type": "integer"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "host_persona_id": "<id from personas>",
                "meeting_type_id": "<id from scheduling>",
                "slug": "book-{{dynamic:uuid}}",
                "title": "Book a discovery call",
                "description": "30-minute discovery call",
                "max_days_ahead": 30,
                "min_notice_minutes": 120,
                "expires_at": "<generated futuredatetime+30d>"
              }
            }
          }
        }
      }
    },
    "/api/scheduling/scheduling-links/{link_id}": {
      "get": {
        "tags": [
          "sdk-scheduling"
        ],
        "summary": "Get a booking link by id",
        "description": "Fetch a single public booking link by id (tenant-scoped). 404 if not found. tenant_id query param required.",
        "operationId": "get_api_scheduling_scheduling_links_link_id",
        "parameters": [
          {
            "name": "link_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "link_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "scheduling_link_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — link not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/scim/v2/Users": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Provision a new user via SCIM 2.0",
        "description": "SCIM 2.0 user provisioning (FR-IDN-9): find-or-create an identity.person from the primary email alias and upsert an identity.tenant_membership for the resolved tenant, mapping user.active to membership status active|suspended. Returns 201 with the SCIM User representation when a new person was created and 200 when an existing email alias was matched, which makes repeated pushes from Okta/Azure AD idempotent. Tenant scoping comes from scimBearerAuth (Authorization: Bearer matched against identity.federation_config.scim_bearer_envelope where protocol='scim' and jit_enabled), falling back to the x-tenant-id header in dev when the stored envelope is NULL. Edge cases: the body's schemas array MUST include urn:ietf:params:scim:schemas:core:2.0:User; a user with no emails[] entry is rejected; /scim/* is NOT on the gateway public allowlist, so the Bearer must also be a valid tenant JWT to clear the default-deny authGate before scimBearerAuth runs.",
        "operationId": "post_scim_v2_Users",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "User_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "schemas": [
                      "urn:ietf:params:scim:schemas:core:2.0:User"
                    ],
                    "userName": "qa.user@example.com",
                    "active": true,
                    "emails": [
                      {
                        "value": "qa.user@example.com",
                        "primary": true
                      }
                    ],
                    "name": {
                      "givenName": "Ada",
                      "familyName": "Lovelace"
                    },
                    "groups": [
                      {
                        "display": "Engineering",
                        "value": "eng"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — No SCIM Bearer token resolved and no x-tenant-id header set"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <not found error from provisionScimUser>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "schemas": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "userName": {
                    "type": "string"
                  },
                  "active": {
                    "type": "boolean"
                  },
                  "emails": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "value": {
                          "type": "string"
                        },
                        "primary": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "name": {
                    "type": "object",
                    "properties": {
                      "givenName": {
                        "type": "string"
                      },
                      "familyName": {
                        "type": "string"
                      }
                    }
                  },
                  "groups": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "display": {
                          "type": "string"
                        },
                        "value": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "schemas": [
                  "urn:ietf:params:scim:schemas:core:2.0:User"
                ],
                "userName": "<generated email>",
                "active": true,
                "emails": [
                  {
                    "value": "<generated email>",
                    "primary": true
                  }
                ],
                "name": {
                  "givenName": "Ada",
                  "familyName": "Lovelace"
                },
                "groups": [
                  {
                    "display": "Engineering",
                    "value": "eng"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/scim/v2/Users/{person_id}": {
      "delete": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Deprovision a SCIM-provisioned user (offboards tenant membership)",
        "description": "SCIM 2.0 deprovisioning (FR-IDN-9): soft-offboards a person in the resolved tenant by setting identity.tenant_membership.status = 'offboarded' for that (person_id, tenant_id) pair, replying 204 with no body. The person row, aliases and app identities are deliberately left intact — this is a membership status change, not a delete. Tenant scoping comes from scimBearerAuth (Bearer matched against identity.federation_config.scim_bearer_envelope), falling back to x-tenant-id in dev. Edge cases: the call is fully idempotent and returns 204 even when person_id is unknown or the membership is already offboarded, because the UPDATE simply matches zero rows; it is tenant-scoped, so a person_id belonging to another tenant is a silent no-op; a non-UUID person_id fails the Postgres cast and surfaces as 500; /scim/* is NOT on the gateway public allowlist, so the Bearer must also be a valid tenant JWT to clear the default-deny authGate.",
        "operationId": "delete_scim_v2_Users_person_id",
        "parameters": [
          {
            "name": "person_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "person_id path parameter"
          }
        ],
        "responses": {
          "204": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — No SCIM Bearer token resolved and no x-tenant-id header set"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/search": {
      "post": {
        "tags": [
          "sdk-search"
        ],
        "summary": "Query encounter entities",
        "description": "POST form of the search query endpoint: accepts a full search DSL body (query/size/from/sort) instead of the flat query-string form, then runs the same ABAC-filtered execution as GET /api/search. tenant_id comes from the verified JWT and effective_scopes are resolved server-side from JWT claims — any effective_scopes in the body is discarded. Edge cases: an empty or non-object body fails validation before reaching OpenSearch; `size` is clamped into 0..200 and `from` floored at 0 so oversized page requests are capped, not rejected; `script`/`script_score`/`function_score` clauses are recursively stripped from the DSL (including inside arrays) so a crafted body cannot execute code or escape tenant filtering; querying an entity_kind with no registered index returns 404 IndexNotFound.",
        "operationId": "post_api_search",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "search_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "entity_kind": "encounter",
                    "q": "open",
                    "dsl": {
                      "query": {
                        "term": {
                          "status": "open"
                        }
                      }
                    },
                    "size": 10,
                    "from": 0,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "IndexNotFound — No search index registered for the requested entity_kind"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entity_kind": {
                    "type": "string"
                  },
                  "q": {
                    "type": "string"
                  },
                  "dsl": {
                    "type": "object",
                    "properties": {
                      "query": {
                        "type": "object",
                        "properties": {
                          "term": {
                            "type": "object",
                            "properties": {
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  },
                  "size": {
                    "type": "integer"
                  },
                  "from": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "entity_kind": "encounter",
                "q": "open",
                "dsl": {
                  "query": {
                    "term": {
                      "status": "open"
                    }
                  }
                },
                "size": 10,
                "from": 0
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-search"
        ],
        "summary": "Query encounters with free text",
        "description": "Executes an ABAC-filtered free-text search over an indexed entity_kind for the caller's tenant, reading q/entity_kind/size/from from the query string and returning the hit set. tenant_id is force-injected from the verified JWT and effective_scopes are server-resolved via resolveEffectiveScopes(req.auth) — a caller-supplied effective_scopes in the request is ignored, so scope escalation is not possible. Edge cases: the JWT tenant_id must be a UUID or validation fails even though the caller never supplied it; entity_kind is mandatory and an unregistered entity_kind returns IndexNotFound rather than an empty page; DSL `size` is clamped to 0..200 and `from` floored at 0, so oversized pagination requests are silently capped instead of erroring; `script`, `script_score` and `function_score` clauses are stripped from any supplied DSL to prevent ABAC bypass; results are always confined to the JWT tenant so a cross-tenant entity is invisible rather than 403.",
        "operationId": "get_api_search",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "search_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "IndexNotFound — No search index registered for the requested entity_kind"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/search/index": {
      "post": {
        "tags": [
          "sdk-search"
        ],
        "summary": "Register encounter entity index",
        "description": "Registers a search index definition for an entity_kind in the caller's tenant and ensures the backing physical index/alias exists, returning the persisted definition. tenant_id is overwritten from the verified JWT, so a tenant_id in the body cannot target another tenant. Edge cases: entity_kind is mandatory; re-registering the same tenant_id + entity_kind is idempotent at the ensureIndex level rather than a duplicate error; omitting opensearch_alias lets the service derive one; field_mappings must be an object or it is dropped; if the search backend is unreachable the ensure step surfaces as 500, and a malformed mapping rejected by the engine also lands as 500 rather than 400.",
        "operationId": "post_api_search_index",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "index_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "entity_kind": "encounter",
                    "opensearch_alias": "ten-0-encounter",
                    "field_mappings": {
                      "properties": {
                        "tenant_id": {
                          "type": "keyword"
                        },
                        "_scope_tags": {
                          "type": "keyword"
                        },
                        "status": {
                          "type": "keyword"
                        }
                      }
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "IndexNotFound — Referenced index could not be resolved"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "entity_kind": {
                    "type": "string"
                  },
                  "opensearch_alias": {
                    "type": "string"
                  },
                  "field_mappings": {
                    "type": "object",
                    "properties": {
                      "properties": {
                        "type": "object",
                        "properties": {
                          "tenant_id": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string"
                              }
                            }
                          },
                          "_scope_tags": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string"
                              }
                            }
                          },
                          "status": {
                            "type": "object",
                            "properties": {
                              "type": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "entity_kind": "encounter",
                "opensearch_alias": "ten-0-encounter",
                "field_mappings": {
                  "properties": {
                    "tenant_id": {
                      "type": "keyword"
                    },
                    "_scope_tags": {
                      "type": "keyword"
                    },
                    "status": {
                      "type": "keyword"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/search/saved-queries": {
      "get": {
        "tags": [
          "sdk-search"
        ],
        "summary": "List my saved queries",
        "description": "Lists the saved queries belonging to one persona within the caller's tenant, scoped by the JWT tenant_id plus the required persona_id query param. Edge cases: persona_id is mandatory and a missing or blank value is a 400 — it is not treated as \"list all\"; a persona that exists but owns nothing returns 200 with an empty queries array rather than 404; a persona_id from another tenant yields an empty list because the tenant filter comes from the JWT, so this endpoint cannot be used to enumerate other tenants' saved queries; there is no pagination, so the full set for the persona is returned in one response.",
        "operationId": "get_api_search_saved_queries",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "saved_query_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — persona_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "IndexNotFound — Referenced index could not be resolved"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-search"
        ],
        "summary": "Save my-open-encounters query",
        "description": "Persists a named saved query (a reusable search DSL) for a persona inside the caller's tenant and returns the stored record. tenant_id is taken from the verified JWT, never the body. Edge cases: both tenant_id and persona_id must be UUIDs and name must be non-blank — all three are reported together in one 400 details array; the dsl key must be an object (a JSON string or array fails); the stored DSL is sanitized exactly like a live query, so script/script_score/function_score clauses are stripped and size is clamped to 0..200 before persistence, meaning a saved query can never be replayed with an escalated clause; names are not uniqueness-checked, so repeated submissions create duplicate saved queries rather than returning a conflict.",
        "operationId": "post_api_search_saved_queries",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "saved_query_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "my-open-encounters",
                    "dsl": {
                      "query": {
                        "term": {
                          "status": "open"
                        }
                      },
                      "size": 50
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — JWT missing tenant_id claim"
          },
          "404": {
            "description": "IndexNotFound — Referenced index could not be resolved"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "dsl": {
                    "type": "object",
                    "properties": {
                      "query": {
                        "type": "object",
                        "properties": {
                          "term": {
                            "type": "object",
                            "properties": {
                              "status": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      },
                      "size": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from auth>",
                "name": "my-open-encounters",
                "dsl": {
                  "query": {
                    "term": {
                      "status": "open"
                    }
                  },
                  "size": 50
                }
              }
            }
          }
        }
      }
    },
    "/api/secrets/{ref}": {
      "get": {
        "tags": [
          "sdk-secrets"
        ],
        "summary": "Retrieve a previously registered SecretRef by ref",
        "description": "Looks up a SecretRef in the catalog by its ref string, which is URL-encoded in the path and decoded by the handler. Returns the catalog record (ref, scope, kms_key_id, rotation metadata) and never the secret material itself. Requires a valid tenant JWT (requireAuth). Edge cases: the ref path segment MUST be percent-encoded, since refs typically contain \"/\" and \":\" characters that would otherwise split into extra path segments and route-miss to a 404; a malformed percent sequence (for example a bare \"%\") makes decodeURIComponent throw a URIError, which is caught and reported as a 500 InternalError rather than a 400; an unknown but well-formed ref returns 404 NotFound with the decoded ref echoed in details[]; the caller JWT tenant is never compared to the record, so any authenticated caller can resolve any ref.",
        "operationId": "get_api_secrets_ref",
        "parameters": [
          {
            "name": "ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ref path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "secret_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No SecretRef registered for <ref>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/secrets/{ref}/rotate": {
      "post": {
        "tags": [
          "sdk-secrets"
        ],
        "summary": "Rotate previously registered SecretRef",
        "description": "Rotates the KMS key version backing a registered SecretRef and stamps rotated_at on the catalog row, returning the rotation result. The ref is URL-encoded in the path and decoded by the handler. Requires a valid tenant JWT (requireAuth). Edge cases: no body is read, so any payload is ignored; the ref MUST be percent-encoded because refs typically contain \"/\" and \":\" which would otherwise split into extra path segments and route-miss to a 404, and a malformed percent sequence makes decodeURIComponent throw a URIError that is caught as a 500 rather than a 400; rotating an unregistered ref is a 404 NotFound, matched on the service message starting with \"Secret reference not registered\"; rotation is repeatable but not idempotent - each call mints a new key version and rewrites rotated_at, so callers must not retry blindly; a KMS-side failure is a 500 InternalError, indistinguishable in status from a DB failure.",
        "operationId": "post_api_secrets_ref_rotate",
        "parameters": [
          {
            "name": "ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ref path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "rotate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Secret reference not registered: <ref>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/secrets": {
      "post": {
        "tags": [
          "sdk-secrets"
        ],
        "summary": "Register a tenant-scoped SecretRef",
        "description": "Registers a new SecretRef in the secrets catalog (ref, scope, kms_key_id) per P1-Foundation-Spine section 5 and returns 201 with the record. This stores only the reference and KMS key binding - no secret material is transmitted or persisted here. Requires a valid tenant JWT (requireAuth). Edge cases: ref, scope and kms_key_id are all mandatory, and scope is checked against a fixed allowlist so an unknown scope 400s with the permitted values echoed in the message; the ref string must also parse as a valid secret reference - a syntactically bad ref passes the presence check and is rejected later by storeSecret as a second, distinct 400 (\"Invalid secret reference\"); kms_key_id is presence-checked only, so a non-existent KMS key is not detected here; the endpoint is not idempotent - re-registering an existing ref trips a unique constraint and surfaces as a 500 InternalError rather than a 409.",
        "operationId": "post_api_secrets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "secret_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ref": "secret://tenant/dev-test-key-001",
                    "scope": "tenant",
                    "kms_key_id": "mock-key-1",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — ref is required / scope is required / scope must be one of <allowed scopes> / kms_key_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  },
                  "kms_key_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "ref": "secret://tenant/dev-test-key-001",
                "scope": "tenant",
                "kms_key_id": "mock-key-1"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-secrets"
        ],
        "summary": "Retrieve a previously registered SecretRef by ref",
        "description": "Looks up a SecretRef in the catalog by its ref string, passed as the `ref` QUERY parameter, and returns the catalog record (ref, scope, kms_key_id, rotation metadata) - never the secret material itself. Requires a valid tenant JWT (requireAuth). WHY THE QUERY FORM: secretRefCatalog validates every reference against /^secret:\\/\\/(app|pool|tenant)\\/(.+)$/, so a conformant ref ALWAYS contains '://' and at least one further '/' - three or more path segments. A Fastify ':ref' parameter matches exactly ONE segment, so the sibling route GET /api/secrets/:ref can never match a real ref and 404s on every conformant value; it is retained in the code only as a harmless legacy path. The query string is where a slash is not a delimiter, so this is the form that actually works and the one QA must use. Edge cases: an absent or whitespace-only ref returns 400 ValidationError naming the expected shape; an unknown but well-formed ref returns 404 NotFound with the ref echoed in details[]; the caller JWT tenant is never compared to the record, so any authenticated caller can resolve any ref.",
        "operationId": "get_api_secrets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — ref query parameter is required, e.g. ?ref=secret://tenant/my-key"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No SecretRef registered for <ref>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sequence-templates": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Create an email welcome template",
        "description": "Create a reusable message template (subject/body per channel) that sequence steps can reference. tenant_id and name are required; channel defaults to 'email'. UNIQUE per (tenant, name).",
        "operationId": "post_api_sequence_templates",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sequence_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "channel": "email",
                    "subject": "Welcome aboard",
                    "body": "Hi {{name}}, welcome!",
                    "category": "custom",
                    "variables": [
                      "name"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and name are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  },
                  "variables": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "channel": "email",
                "subject": "Welcome aboard",
                "body": "Hi {{name}}, welcome!",
                "category": "custom",
                "variables": [
                  "name"
                ]
              }
            }
          }
        }
      }
    },
    "/api/sequences/enrollments/{enrollment_id}/control": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Pause the enrollment on inbound reply",
        "description": "Reactively control an enrollment's cadence. action='pause' (pause-on-reply) moves queued steps to 'paused'; 'resume' returns them to 'pending'; 'stop' (stop-on-optout/payment) cancels every still-queued/paused step; 'replace_cta' swaps the template on upcoming steps (requires template_id). reason + event are captured on the affected rows. Returns the number of steps affected. tenant_id and action are required; 400 on an unknown action or a replace_cta without template_id.",
        "operationId": "post_api_sequences_enrollments_enrollment_id_control",
        "parameters": [
          {
            "name": "enrollment_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "enrollment_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "control_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "action": "pause",
                    "reason": "reply",
                    "event": "inbound.reply",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and action are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "event": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "action": "pause",
                "reason": "reply",
                "event": "inbound.reply"
              }
            }
          }
        }
      }
    },
    "/api/sequences/guards/check": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Fresh subject is allowed",
        "description": "Evaluate the send guards for a prospective touch and record the decision to the guard audit log. Blocks on: per-lead cooldown (min gap since the subject's last sent touch), max-messages over the rolling window, duplicate content (dedupe_hash already allowed in-window), or an open circuit breaker for the (tenant, channel). A fresh subject with no history is allowed. tenant_id, subject_persona_id and channel are required.",
        "operationId": "post_api_sequences_guards_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "check_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, subject_persona_id and channel are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_persona_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_persona_id": "<id from personas>",
                "channel": "email"
              }
            }
          }
        }
      }
    },
    "/api/sequences/guards/log": {
      "get": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "List the tenant's guard log (empty is valid)",
        "description": "List the guard decision audit trail for a tenant, newest first. Each entry records the decision (allow/block), the reason (cooldown/max_messages/duplicate/circuit_open), subject, channel and any dedupe_hash. Tenant-scoped via the required tenant_id query param; optional decision filter (allow|block) and limit. Returns an empty array when no guard checks have run.",
        "operationId": "get_api_sequences_guards_log",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "log_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sequences/guards/outcome": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Record a successful send (breaker stays closed)",
        "description": "Record a send outcome for the per-(tenant, channel) circuit breaker. success=true resets the failure streak and closes a half-open breaker; success=false increments the streak and opens the breaker once breaker_failure_threshold is reached. Returns the breaker state (closed/open/half_open) + failure/success counts. tenant_id, channel and success (boolean) are required.",
        "operationId": "post_api_sequences_guards_outcome",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "outcome_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "email",
                    "success": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, channel and success (boolean) are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "success": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "success": true
              }
            }
          }
        }
      }
    },
    "/api/sequences": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Create a lead welcome cadence",
        "description": "Create a multi-touch cadence sequence for a tenant. Steps and triggers are added via sub-resource endpoints. tenant_id and name are required; sequence_type defaults to 'lead'. Returns the created sequence (status defaults to 'active').",
        "operationId": "post_api_sequences",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sequence_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "description": "Welcome cadence for new leads",
                    "sequence_type": "lead",
                    "is_default": false,
                    "metadata": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and name are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "sequence_type": {
                    "type": "string"
                  },
                  "is_default": {
                    "type": "boolean"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "description": "Welcome cadence for new leads",
                "sequence_type": "lead",
                "is_default": false,
                "metadata": {}
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "List every sequence in the tenant",
        "description": "Every sequence defined for a tenant, returned as data.sequences. tenant_id is a REQUIRED QUERY PARAMETER, not a claim: listSequences takes it directly, so omitting it is 400 rather than defaulting to the caller's tenant. Takes no other filter - status and channel query parameters are ignored rather than narrowing the result. Returns the sequence definitions themselves, not enrolments or step state; a caller tracking a contact through a sequence wants the enrolment routes instead. An empty tenant answers 200 with an empty array rather than 404, because 'this tenant has defined no sequences' is an answer.",
        "operationId": "get_api_sequences",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sequences/{sequence_id}/enroll": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Enroll a persona into the sequence",
        "description": "Enroll an L4 persona into a sequence (event-based: form_submit/reply/stage_change/manual). Idempotent — if the persona already has an active run in this sequence it returns the existing enrollment (200) and schedules nothing new; otherwise it creates a new enrollment and seeds the first step as a due execution_step (201). The sequence must have at least one step (else 409).",
        "operationId": "post_api_sequences_sequence_id_enroll",
        "parameters": [
          {
            "name": "sequence_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sequence_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "enroll_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "event_type": "form_submit",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and subject_persona_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "409": {
            "description": "EnrollFailed — sequence has no steps to enroll into"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_persona_id": {
                    "type": "string"
                  },
                  "event_type": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_persona_id": "<id from personas>",
                "event_type": "form_submit"
              }
            }
          }
        }
      }
    },
    "/api/sequences/{sequence_id}": {
      "get": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Get the created sequence",
        "description": "Fetch a single sequence by id, tenant-scoped via the tenant_id query param. 404 if the sequence does not exist for that tenant.",
        "operationId": "get_api_sequences_sequence_id",
        "parameters": [
          {
            "name": "sequence_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sequence_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sequence_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sequences/{sequence_id}/steps": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Add step 1 (email, 60s delay)",
        "description": "Add an ordered step to a sequence. Steps are UNIQUE per (sequence, step_number). template_id (optional) references a sequence-template. schedule_mode/delay_seconds drive when the executor sends the step relative to enrollment. tenant_id and step_number are required.",
        "operationId": "post_api_sequences_sequence_id_steps",
        "parameters": [
          {
            "name": "sequence_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sequence_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "step_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "step_number": 1,
                    "channel": "email",
                    "action": "send",
                    "template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject": "Welcome aboard",
                    "body": "Hi there!",
                    "schedule_mode": "delay",
                    "delay_seconds": 60,
                    "send_mode": "individual",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and step_number are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "step_number": {
                    "type": "integer"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string"
                  },
                  "template_id": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  },
                  "schedule_mode": {
                    "type": "string"
                  },
                  "delay_seconds": {
                    "type": "integer"
                  },
                  "send_mode": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "step_number": 1,
                "channel": "email",
                "action": "send",
                "template_id": "<id from sequence-templates>",
                "subject": "Welcome aboard",
                "body": "Hi there!",
                "schedule_mode": "delay",
                "delay_seconds": 60,
                "send_mode": "individual"
              }
            }
          }
        }
      }
    },
    "/api/sequences/{sequence_id}/triggers": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Enroll on form submission",
        "description": "Create (or upsert) an event-based enrollment trigger for a sequence: form_submit / reply / stage_change / manual / booking / tag_added. For stage_change, stage_id + trigger_on (enter/exit) apply. Idempotent per (sequence, event_type, stage_id, trigger_on). tenant_id is required.",
        "operationId": "post_api_sequences_sequence_id_triggers",
        "parameters": [
          {
            "name": "sequence_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sequence_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "trigger_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "event_type": "form_submit",
                    "trigger_on": "enter",
                    "condition_json": {},
                    "enabled": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "event_type": {
                    "type": "string"
                  },
                  "trigger_on": {
                    "type": "string"
                  },
                  "condition_json": {
                    "type": "object",
                    "properties": {}
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "event_type": "form_submit",
                "trigger_on": "enter",
                "condition_json": {},
                "enabled": true
              }
            }
          }
        }
      }
    },
    "/api/sequences/tick": {
      "post": {
        "tags": [
          "sdk-sequence"
        ],
        "summary": "Run a tick (nothing due -> zero counts)",
        "description": "Drive one durable step-executor tick on demand: claims up to batch_size (default 50) due execution steps (status pending/scheduled/deferred, next_run_at past) with FOR UPDATE SKIP LOCKED, gates each send against the sequence's send-window / quiet hours (deferring out-of-window touches to the next open slot), applies the frequency-cap + circuit-breaker guards (a blocked touch is 'skipped' and the cadence advances), emits allowed in-window touches via the pluggable step sender ('wait' actions just complete), records the send outcome to the channel breaker, and idempotently enqueues the next step (dedupe_key + ON CONFLICT DO NOTHING). The same logic runs on a timer when SEQUENCE_EXECUTOR_ENABLED. Returns per-tick counts; all zero when nothing is due.",
        "operationId": "post_api_sequences_tick",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tick_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "batch_size": 50,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "batch_size": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "batch_size": 50
              }
            }
          }
        }
      }
    },
    "/api/service-request/queues": {
      "post": {
        "tags": [
          "sdk-service-request"
        ],
        "summary": "Create a service-request queue (tenant_id + name required)",
        "description": "Creates a service-request queue for a tenant with a display name and an optional numeric routing priority, returning the created queue with 201. Edge cases: tenant_id and name are both mandatory and a missing one produces the generic \"missing fields\" ValidationError; priority is optional and defaulted by the service when omitted; name uniqueness is not enforced at the route, so repeated calls with the same name create duplicate queues unless a database unique constraint rejects it (which surfaces as a 500, not a 409); tenant_id is taken from the body rather than the JWT, so this route does not itself prevent creating a queue under another tenant id — it only requires a valid token.",
        "operationId": "post_api_service_request_queues",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "queue_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "priority": 100,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "priority": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "<generated name>",
                "priority": 100
              }
            }
          }
        }
      }
    },
    "/api/service-request/tickets": {
      "post": {
        "tags": [
          "sdk-service-request"
        ],
        "summary": "Open a ticket (tenant_id + encounter_id + requester_persona_id required)",
        "description": "Creates a service-request ticket against an encounter for a requesting persona, optionally placing it on a queue with a priority/severity and external reference map, and returns the created ticket with 201. Edge cases: tenant_id, encounter_id and requester_persona_id are all mandatory and a missing one yields a single generic ValidationError (\"missing fields\") without naming the offender; queue_id, priority, severity and external_refs are optional and defaulted by the service; priority/severity are not enum-validated at the route, so an out-of-range value reaches the insert and surfaces as a 500 rather than a 400; a queue_id or encounter_id that does not exist fails the foreign key at insert time and also surfaces as a 500; there is no idempotency key, so re-POSTing the same body creates a second distinct ticket.",
        "operationId": "post_api_service_request_tickets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ticket_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "encounter_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "requester_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "queue_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "priority": "normal",
                    "severity": "minor",
                    "external_refs": {
                      "zendesk_id": "ZD-1001"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "encounter_id": {
                    "type": "string"
                  },
                  "requester_persona_id": {
                    "type": "string"
                  },
                  "queue_id": {
                    "type": "string"
                  },
                  "priority": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string"
                  },
                  "external_refs": {
                    "type": "object",
                    "properties": {
                      "zendesk_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "encounter_id": "<id from encounters>",
                "requester_persona_id": "<id from personas>",
                "queue_id": "<id from service-request>",
                "priority": "normal",
                "severity": "minor",
                "external_refs": {
                  "zendesk_id": "ZD-1001"
                }
              }
            }
          }
        }
      }
    },
    "/api/service-request/tickets/{ticket_id}/assign": {
      "post": {
        "tags": [
          "sdk-service-request"
        ],
        "summary": "Assign an existing ticket to a persona (assignee_persona_id required)",
        "description": "Assigns an existing ticket to an agent persona by setting assignee_persona_id, returning the updated ticket. Edge cases: assignee_persona_id is required in the body and a missing value returns 400 before any lookup; an unknown ticket_id returns 404; assignment is a plain overwrite, so re-assigning an already-assigned ticket succeeds and silently replaces the previous assignee rather than returning a conflict, which makes the call idempotent for the same persona; the endpoint does not check ticket status, so a resolved or closed ticket can still be re-assigned; an assignee_persona_id that does not exist violates the foreign key at update time and surfaces as a 500.",
        "operationId": "post_api_service_request_tickets_ticket_id_assign",
        "parameters": [
          {
            "name": "ticket_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ticket_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "assign_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "assignee_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing assignee_persona_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assignee_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "assignee_persona_id": "<id from personas>"
              }
            }
          }
        }
      }
    },
    "/api/service-request/tickets/{ticket_id}": {
      "get": {
        "tags": [
          "sdk-service-request"
        ],
        "summary": "Fetch a ticket by id",
        "description": "Fetches a single service-request ticket by its path ticket_id and returns the full ticket record. Edge cases: an unknown ticket_id returns 404 NotFound with no body detail; a malformed (non-UUID) ticket_id makes the lookup query fail on type cast and surfaces as a 500 rather than a 400, since the route performs no id-format validation; the handler does not compare the ticket's tenant against the caller's JWT tenant_id, so scoping relies on ticket ids being unguessable — a valid ticket_id from another tenant is still readable by any authenticated caller.",
        "operationId": "get_api_service_request_tickets_ticket_id",
        "parameters": [
          {
            "name": "ticket_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ticket_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "ticket_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/service-request/tickets/{ticket_id}/transition": {
      "post": {
        "tags": [
          "sdk-service-request"
        ],
        "summary": "Transition a new ticket to in-progress (valid transition from default status 'new')",
        "description": "Advances a ticket to a new lifecycle status (new, in-progress, awaiting-customer, resolved, closed) and returns the updated ticket. Edge cases: `to` is validated against the allowed status list at the route, so an unknown or misspelled status is a 400 before any read; a legal status that is not reachable from the ticket's current status is rejected by transitionTicket with a 409 InvalidTransition carrying the \"current → target\" message; an unknown ticket_id returns 404; transitioning a ticket to the status it already holds is treated as an invalid transition rather than a no-op, so this call is not idempotent; the guard is state-machine based, meaning e.g. a closed ticket cannot be reopened through this route.",
        "operationId": "post_api_service_request_tickets_ticket_id_transition",
        "parameters": [
          {
            "name": "ticket_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "ticket_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to": "in-progress",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — invalid target status"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "409": {
            "description": "InvalidTransition — Invalid ticket transition <current> → <to>"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "to": "in-progress"
              }
            }
          }
        }
      }
    },
    "/api/social/handles": {
      "post": {
        "tags": [
          "sdk-social"
        ],
        "summary": "Authorize a social handle for a tenant",
        "description": "Authorizes a social network handle for a tenant and binds it to the persona permitted to act on it, returning 201 with the handle record. Edge cases: tenant_id, network, external_handle_id and authorized_persona_id are all mandatory and a missing one returns the generic \"missing fields\" ValidationError without naming the field; network is enum-checked against twitter, linkedin, instagram, facebook and tiktok and anything else is a separate \"invalid network\" 400; the route performs no duplicate check, so re-authorizing the same tenant + network + external_handle_id either creates a second row or trips a database unique constraint that surfaces as a 500 rather than a 409; tenant_id is read from the body rather than the JWT, so the route itself does not prevent naming another tenant; an authorized_persona_id that does not exist violates a foreign key at insert and also surfaces as 500.",
        "operationId": "post_api_social_handles",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "handle_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "network": "twitter",
                    "external_handle_id": "Acme QA Sample",
                    "authorized_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "network": {
                    "type": "string"
                  },
                  "external_handle_id": {
                    "type": "string"
                  },
                  "authorized_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "network": "twitter",
                "external_handle_id": "<generated name>",
                "authorized_persona_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/social/interactions/{interaction_id}/capture-lead": {
      "post": {
        "tags": [
          "sdk-social"
        ],
        "summary": "Capture an interaction as a CRM lead",
        "description": "Converts a social interaction into a captured lead by linking it to a CRM contact, returning the updated interaction. Edge cases: contact_id is required in the body and a missing value is a 400 before any lookup; an unknown interaction_id returns 404 NotFound; the link is a plain overwrite, so re-capturing an interaction that is already linked replaces the previous contact_id rather than returning a conflict — repeating the same call is effectively idempotent, but pointing it at a different contact silently re-parents the lead; a contact_id that does not exist violates a foreign key at update time and surfaces as a 500 rather than a 404; a malformed (non-UUID) interaction_id fails the query cast and also surfaces as 500.",
        "operationId": "post_api_social_interactions_interaction_id_capture_lead",
        "parameters": [
          {
            "name": "interaction_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "interaction_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capture_lead_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing contact_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "contact_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/social/interactions": {
      "post": {
        "tags": [
          "sdk-social"
        ],
        "summary": "Ingest an inbound social interaction",
        "description": "Ingests an inbound social interaction (DM, comment, mention or review) against a previously authorized handle, recording the external author id and optional message body, and returns 201 with the stored interaction. Edge cases: handle_id, kind and author_external_id are mandatory and a missing one returns the generic \"missing fields\" 400; kind is enum-checked against dm, comment, mention and review with a distinct \"invalid kind\" 400; body is optional so an empty-bodied interaction (for example a bare mention) is accepted; author_persona_id is optional and left null when the external author has not yet been resolved to a known persona; a handle_id that does not exist or was never authorized violates a foreign key at insert and surfaces as a 500 rather than a 404; there is no external-id de-duplication, so replaying the same webhook payload creates duplicate interactions.",
        "operationId": "post_api_social_interactions",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "interaction_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "handle_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "comment",
                    "author_external_id": "Acme QA Sample",
                    "author_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "body": "Loving the new product launch!",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — missing fields"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "handle_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "author_external_id": {
                    "type": "string"
                  },
                  "author_persona_id": {
                    "type": "string"
                  },
                  "body": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "handle_id": "<id from social>",
                "kind": "comment",
                "author_external_id": "<generated name>",
                "author_persona_id": "<id from auth>",
                "body": "Loving the new product launch!"
              }
            }
          }
        }
      }
    },
    "/api/identity/social/{provider}/callback": {
      "post": {
        "tags": [
          "sdk-social"
        ],
        "summary": "Google callback with verified id_token claims",
        "description": "Consumes a verified social IdP assertion (google, apple or microsoft) and federates it into a ProjexCloud identity for the named tenant, returning the resolved person and session material. This path matches the gateway OAuth-callback allowlist regex, so it is deliberately public and takes no bearer token — trust rests entirely on the caller having already verified the provider claims. Edge cases: provider is enum-checked from the path and anything outside google|apple|microsoft is a 400; tenant_id and verified_claims.sub are both mandatory; email, email_verified and name are optional, so an assertion with an unverified or absent email still federates and may create an identity that cannot be matched to an existing person by email; a tenant_id that does not resolve surfaces as 404 via the \"not found\" message branch; repeat callbacks with the same provider subject resolve to the same person rather than creating duplicates.",
        "operationId": "post_api_identity_social_provider_callback",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "provider path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "callback_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "verified_claims": {
                      "sub": "Acme QA Sample",
                      "email": "qa.user@example.com",
                      "email_verified": true
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — provider must be google|apple|microsoft"
          },
          "404": {
            "description": "NotFound — <entity> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "verified_claims": {
                    "type": "object",
                    "properties": {
                      "sub": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "email_verified": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "verified_claims": {
                  "sub": "<generated name>",
                  "email": "<generated email>",
                  "email_verified": true
                }
              }
            }
          }
        }
      }
    },
    "/api/taxonomy/extraction-schemas": {
      "get": {
        "tags": [
          "sdk-taxonomy"
        ],
        "summary": "Lookup active extraction schema by document_kind (tenant with platform fallback)",
        "description": "Looks up the active extraction schema for a document_kind, resolving the tenant-specific override first and falling back to the platform-default schema when tenant_id is omitted or has no override. Edge cases: document_kind is a required query param and its absence is a 400; tenant_id is optional and defaults to null, which selects the global schema — passing a tenant with no override transparently returns the global one rather than 404; a document_kind with no active schema at either level returns 404 with a success:false envelope (note this SDK uses {success, data} rather than the {data} envelope used elsewhere); only the active version is returned, so a schema that exists but was deactivated reads as 404; all datastore failures collapse into a generic 500 \"Lookup failed\" with no detail.",
        "operationId": "get_api_taxonomy_extraction_schemas",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "extraction_schema_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing query param: document_kind"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No active extraction schema for document_kind"
          },
          "500": {
            "description": "InternalError — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/taxonomy/health": {
      "get": {
        "tags": [
          "sdk-taxonomy"
        ],
        "summary": "Health check returns sdk + status ok",
        "description": "Liveness probe for sdk-taxonomy, returning { sdk: \"sdk-taxonomy\", status: \"ok\" }. This route registers no requireAuth preHandler and its path ends in /health, which the gateway default-deny auth gate treats as public, so it answers 200 with no bearer token — QA should assert it is reachable unauthenticated. Edge cases: the response is a static literal that does not touch Postgres or any taxonomy table, so it stays 200 even when the taxonomy datastore is down — it must not be used as a readiness or dependency-health signal; it accepts no parameters, ignores query strings, and has no error branch of its own.",
        "operationId": "get_api_taxonomy_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/taxonomy/prompt-templates": {
      "get": {
        "tags": [
          "sdk-taxonomy"
        ],
        "summary": "Lookup active prompt template by purpose_tag (tenant with platform fallback)",
        "description": "Looks up the active prompt template for a purpose_tag, optionally narrowed by template name, resolving a tenant-specific override before the platform default. Edge cases: purpose_tag is a required query param and its absence is a 400 while name is optional — supplying a name that does not exist narrows the lookup to nothing and returns 404 rather than falling back to the unnamed template for that purpose_tag; tenant_id is optional and defaults to null, selecting the global template, so a tenant with no override silently receives the global one; only active templates are matched, so a deactivated template reads as 404; the response uses the {success, data} envelope; any datastore error collapses into a generic 500 \"Lookup failed\".",
        "operationId": "get_api_taxonomy_prompt_templates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "prompt_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing query param: purpose_tag"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No active prompt template for purpose_tag"
          },
          "500": {
            "description": "InternalError — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/taxonomy/versions/{taxonomy_version_id}/activate": {
      "post": {
        "tags": [
          "sdk-taxonomy"
        ],
        "summary": "Activate a taxonomy version (demotes prior active version to deprecated)",
        "description": "Activates a taxonomy version by id, making it the live version and recording the JWT subject as the actor (falling back to \"system\" when the token carries no sub). Edge cases: an unknown taxonomy_version_id is detected only by string-matching \"not found\" in the thrown error message and returned as 404 — any other service error, including attempting to activate a version that is already active or one belonging to a superseded taxonomy, collapses into a generic 500 \"Activation failed\" with no distinguishing detail; activation supersedes the previously active version rather than erroring, so ordering matters; a malformed (non-UUID) id fails the query cast and also surfaces as 500; the route's own missing-path-param 400 is effectively unreachable because Fastify will not match the route without the segment.",
        "operationId": "post_api_taxonomy_versions_taxonomy_version_id_activate",
        "parameters": [
          {
            "name": "taxonomy_version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "taxonomy_version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "activate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "taxonomy_version_id": "{{var:taxonomy_version_id}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Missing path param: taxonomy_version_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error message containing \"not found\">"
          },
          "500": {
            "description": "InternalError — Activation failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "taxonomy_version_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "taxonomy_version_id": "{{var:taxonomy_version_id}}"
              }
            }
          }
        }
      }
    },
    "/api/tenant-lifecycle/{tenant_id}/offboard": {
      "post": {
        "tags": [
          "sdk-tenant-lifecycle"
        ],
        "summary": "Begin offboarding (active -> offboarding)",
        "description": "Starts tenant offboarding and stamps a data-retention deadline, defaulting to 30 days from now (per FR-TLC-6) when deadline_at is omitted, and returns the resulting lifecycle state. Edge cases: ownership is checked first — a JWT whose tenant_id and parent_tenant_id both differ from the path tenant_id gets 403; deadline_at must parse as a date, and any unparseable string (including a non-ISO format) yields 400 \"deadline_at must be ISO-8601\"; a deadline in the past is accepted by the route because only NaN is rejected, so back-dated deadlines are not guarded here; offboarding a tenant that is already offboarding or in a state with no legal edge to offboarded returns 409, making repeat calls non-idempotent.",
        "operationId": "post_api_tenant_lifecycle_tenant_id_offboard",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "offboard_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "deadline_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — deadline_at must be ISO-8601"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Tenant ownership check failed"
          },
          "409": {
            "description": "InvalidTransition — Invalid tenant lifecycle transition <from> → offboarding"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "deadline_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "deadline_at": "<generated futuredatetime>"
              }
            }
          }
        }
      }
    },
    "/api/tenant-lifecycle/sandbox": {
      "post": {
        "tags": [
          "sdk-tenant-lifecycle"
        ],
        "summary": "Create sandbox sub-tenant from caller's tenant_id",
        "description": "Creates a sandbox tenant beneath the caller's own tenant, optionally with an expiry timestamp and a data sanitization policy, returning the sandbox record with 201. The parent tenant is taken from the JWT tenant_id — it is never read from the body, so a sandbox cannot be attached to a tenant the caller does not hold a token for. Edge cases: a token with no tenant_id claim is rejected 403 before any work; expires_at is optional but when present must parse as a date, otherwise 400; an expires_at in the past is not rejected by the route; sanitization_policy is free-form here and validated downstream; there is no idempotency key or name-uniqueness check, so repeated calls create additional sandbox tenants.",
        "operationId": "post_api_tenant_lifecycle_sandbox",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sandbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "expires_at": "2026-01-15T10:30:00Z",
                    "sanitization_policy": "default-mask-pii",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — expires_at must be ISO-8601"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Caller must have tenant_id"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires_at": {
                    "type": "string"
                  },
                  "sanitization_policy": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "expires_at": "<generated futuredatetime>",
                "sanitization_policy": "default-mask-pii"
              }
            }
          }
        }
      }
    },
    "/api/tenant-lifecycle/{tenant_id}/suspend": {
      "post": {
        "tags": [
          "sdk-tenant-lifecycle"
        ],
        "summary": "Suspend tenant (active -> suspended)",
        "description": "Suspends a tenant with a mandatory human-readable reason and returns the resulting lifecycle state, recording the JWT subject as the acting actor (falling back to \"api-gateway\" when the token has no sub). Edge cases: the caller must own the tenant — the JWT tenant_id must equal the path tenant_id, or its parent_tenant_id must (the reseller-attached path per FR-TLC-7) — otherwise 403 before any state read; reason is required and an empty string is rejected; suspending a tenant that is already suspended, or one in a terminal offboarded state, is rejected by the state machine with 409 InvalidTransition, so this call is not idempotent; an unknown tenant_id fails the ownership check first and returns 403 rather than 404.",
        "operationId": "post_api_tenant_lifecycle_tenant_id_suspend",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "suspend_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "non-payment",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — reason is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Tenant ownership check failed"
          },
          "409": {
            "description": "InvalidTransition — Invalid tenant lifecycle transition <from> → suspended"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "non-payment"
              }
            }
          }
        }
      }
    },
    "/api/tenant-lifecycle/{tenant_id}/reinstate": {
      "post": {
        "tags": [
          "sdk-tenant-lifecycle"
        ],
        "summary": "Reinstate tenant (suspended -> active)",
        "description": "Reinstates a previously suspended tenant back to its active lifecycle state, attributing the change to the JWT subject. Takes no body. Edge cases: ownership is enforced the same way as suspend — the JWT tenant_id or parent_tenant_id must match the path tenant_id, otherwise 403; reinstating a tenant that is already active is an invalid transition (409), so the call is not idempotent; a tenant that has been offboarded past its deadline cannot be reinstated through this route and also returns 409 with the \"from → active\" message; an unknown tenant_id is caught by the ownership check and returns 403 rather than 404.",
        "operationId": "post_api_tenant_lifecycle_tenant_id_reinstate",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reinstate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Tenant ownership check failed"
          },
          "409": {
            "description": "InvalidTransition — Invalid tenant lifecycle transition <from> → active"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/tenant-lifecycle/{tenant_id}/state": {
      "get": {
        "tags": [
          "sdk-tenant-lifecycle"
        ],
        "summary": "Read state after a transition",
        "description": "Reads the current lifecycle state record for a tenant (active / suspended / offboarding and its associated metadata such as reason, actor and deadline). Edge cases: ownership is enforced before the read — the JWT tenant_id or parent_tenant_id must equal the path tenant_id, so probing another tenant's state returns 403, not 404; a tenant the caller does own but which has no lifecycle row yet (never suspended, reinstated or offboarded) returns 404 NotFound rather than a synthesized \"active\" default, so consumers must treat 404 as \"no lifecycle event recorded\"; a malformed tenant_id fails the ownership comparison and returns 403 rather than a validation error.",
        "operationId": "get_api_tenant_lifecycle_tenant_id_state",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "state_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Tenant ownership check failed"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/tenants": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Create a root tenant under the self-serve app",
        "description": "Provisions a top-level tenant. app_id, display_name and region are required; isolation_tier (S | P | G) plus the pool/reseller/geo/brand fields are optional. Edge cases: isolation_tier is whitelist-checked, so an unknown tier is a 400, while all other optional fields are passed through unvalidated; a parent_tenant_id, reseller_id or geo_node_id referencing no row is caught as a foreign-key violation and mapped to 400 ValidationError (NOT 404); a duplicate natural key such as brand_domain maps to 409 Conflict; module_subscriptions must be an array or it is silently dropped; a whitespace-only display_name trims to empty and is rejected.",
        "operationId": "post_api_tenants",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "display_name": "Acme QA Sample",
                    "region": "us-east-1",
                    "isolation_tier": "S",
                    "brand_domain": "acme.example.com",
                    "admin_pool_index": "admin",
                    "app_pool_index": {},
                    "module_subscriptions": [],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — app_id is required / display_name is required / region is required / isolation_tier must be one of S, P, G"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error text containing 'not found'>"
          },
          "409": {
            "description": "Conflict — <pg error: duplicate key value violates unique constraint ...>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app_id": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "isolation_tier": {
                    "type": "string"
                  },
                  "brand_domain": {
                    "type": "string"
                  },
                  "admin_pool_index": {
                    "type": "string"
                  },
                  "app_pool_index": {
                    "type": "object",
                    "properties": {}
                  },
                  "module_subscriptions": {
                    "type": "array",
                    "items": {}
                  }
                }
              },
              "example": {
                "app_id": "<id from auth>",
                "display_name": "<generated name>",
                "region": "us-east-1",
                "isolation_tier": "S",
                "brand_domain": "acme.example.com",
                "admin_pool_index": "admin",
                "app_pool_index": {},
                "module_subscriptions": []
              }
            }
          }
        }
      }
    },
    "/api/tenants/{tenant_id}/bus": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Create a Region root BU",
        "description": "Creates a business unit under :tenant_id. name and kind are required; parent_bu_id is optional and nests the BU inside another BU. Edge cases: kind is free text here — only presence is checked, so an unsupported kind is rejected by the DB (surfacing as 500) rather than by a 400; an unknown :tenant_id or a parent_bu_id that does not exist is a foreign-key violation mapped to 400 ValidationError, NOT 404; a duplicate BU name within the tenant maps to 409 Conflict where a unique constraint exists; the handler does not guard against a parent_bu_id belonging to a different tenant, nor against creating a cycle in the BU tree.",
        "operationId": "post_api_tenants_tenant_id_bus",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "bus_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Acme QA Sample",
                    "kind": "region",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — name is required / kind is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error text containing 'not found'>"
          },
          "409": {
            "description": "Conflict — <pg error: duplicate key value violates unique constraint ...>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "<generated name>",
                "kind": "region"
              }
            }
          }
        }
      }
    },
    "/api/tenants/{tenant_id}/fiscal-calendar": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Set fiscal year starting in April with USD",
        "description": "Defines the tenant's fiscal calendar and generates its periods, returning the generated period rows with 201. year_start_month (integer 1..12) and base_currency (exactly 3 characters, ISO-4217) are required; period_kind (year | quarter | month | week) is optional and whitelist-checked. Edge cases: year_start_month is range-checked so 0 or 13 is a 400, but a non-integer such as 6.5 passes the Number.isFinite check and reaches the service; base_currency is checked for LENGTH ONLY, so 'XXX' or '123' is accepted as a currency; an unknown :tenant_id is a foreign-key violation mapped to 400 ValidationError, not 404; re-posting a calendar for a tenant that already has one trips the period unique constraint and maps to 409 Conflict, so this route is not safely repeatable.",
        "operationId": "post_api_tenants_tenant_id_fiscal_calendar",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "fiscal_calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "year_start_month": 4,
                    "base_currency": "USD",
                    "period_kind": "quarter",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — year_start_month must be an integer 1..12 / base_currency must be an ISO-4217 3-letter code / period_kind must be one of year, quarter, month, week"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error text containing 'not found'>"
          },
          "409": {
            "description": "Conflict — <pg error: duplicate key value violates unique constraint ...>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "year_start_month": {
                    "type": "integer"
                  },
                  "base_currency": {
                    "type": "string"
                  },
                  "period_kind": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "year_start_month": 4,
                "base_currency": "USD",
                "period_kind": "quarter"
              }
            }
          }
        }
      }
    },
    "/api/tenants/{tenant_id}/reseller-attach": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Attach a reseller to a tenant with custom commission",
        "description": "Attaches an existing reseller to :tenant_id and optionally overrides the commission_rules for that pairing. Returns the updated tenant with 200 (not 201). Edge cases: reseller_id is the only required field; commission_rules must be an object or it is silently DROPPED (a string or array is discarded, not rejected); an unknown reseller_id or :tenant_id is a foreign-key violation mapped to 400 ValidationError rather than 404 — unless the service raises a 'not found' message, which maps to 404; re-attaching the SAME reseller either overwrites the attachment or trips a unique constraint mapped to 409 Conflict, so retries are not safely idempotent.",
        "operationId": "post_api_tenants_tenant_id_reseller_attach",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reseller_attach_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reseller_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "commission_rules": {
                      "default_pct": 12
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — reseller_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error text containing 'not found'>"
          },
          "409": {
            "description": "Conflict — <pg error: duplicate key value violates unique constraint ...>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reseller_id": {
                    "type": "string"
                  },
                  "commission_rules": {
                    "type": "object",
                    "properties": {
                      "default_pct": {
                        "type": "integer"
                      }
                    }
                  }
                }
              },
              "example": {
                "reseller_id": "<id from resellers>",
                "commission_rules": {
                  "default_pct": 12
                }
              }
            }
          }
        }
      }
    },
    "/api/tenants/{tenant_id}/sub-tenants": {
      "post": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Create a sub-tenant that shares parent pool",
        "description": "Creates a sub-tenant beneath :tenant_id. app_id, display_name and region are required; placement (share | tier-p | tier-g) decides whether the child shares the parent's pool or gets a dedicated tier. Edge cases: placement is whitelist-checked only when supplied — omitting it is legal and defers to the service default; the PARENT :tenant_id is not validated by the handler, so an unknown or non-UUID parent surfaces as a foreign-key violation mapped to 400 ValidationError, not 404; duplicate natural keys map to 409 Conflict; nesting depth and per-parent sub-tenant counts are not capped at this layer.",
        "operationId": "post_api_tenants_tenant_id_sub_tenants",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sub_tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "app_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "display_name": "Acme QA Sample",
                    "region": "us-east-1",
                    "placement": "share",
                    "isolation_tier": "S",
                    "brand_domain": "sub.example.com",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — app_id is required / display_name is required / region is required / placement must be one of share, tier-p, tier-g"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <error text containing 'not found'>"
          },
          "409": {
            "description": "Conflict — <pg error: duplicate key value violates unique constraint ...>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app_id": {
                    "type": "string"
                  },
                  "display_name": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "placement": {
                    "type": "string"
                  },
                  "isolation_tier": {
                    "type": "string"
                  },
                  "brand_domain": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "app_id": "<id from auth>",
                "display_name": "<generated name>",
                "region": "us-east-1",
                "placement": "share",
                "isolation_tier": "S",
                "brand_domain": "sub.example.com"
              }
            }
          }
        }
      }
    },
    "/api/tenants/{tenant_id}/contact": {
      "get": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Resolve the founding member as the tenant contact",
        "description": "Resolves a tenant's primary/billing contact from its FOUNDING member (earliest active identity.tenant_membership row): display_name from the L2 profile band, email and phone from the person's aliases. Purpose-bound, consent-gated and audited (TK-3572). Edge cases: ?purpose= defaults to 'support'; the internal purposes support/billing/operations are TPO-allowed, but ANY other purpose requires an active, unrevoked, unexpired consent.receipt for that person and FAILS CLOSED with 403 consent_absent when none exists; a tenant with no active members returns 404 even though the tenant itself exists; every access — granted or denied — writes a consent.contact_read.{granted,denied}.v1 audit event, and audit failures are swallowed so they never block the decision; a non-UUID tenant_id fails the ::uuid cast and returns 500.",
        "operationId": "get_api_tenants_tenant_id_contact",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "contact_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "consent_absent — reading tenant contact for purpose '<purpose>' requires an active consent receipt"
          },
          "404": {
            "description": "NotFound — No active member found for tenant"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": []
      }
    },
    "/api/tenants/{tenant_id}": {
      "get": {
        "tags": [
          "sdk-tenant"
        ],
        "summary": "Fetch the self-serve tenant by id",
        "description": "Fetches a tenant by id. Edge cases: an unknown :tenant_id returns 404 with the id echoed in details[]; the lookup is by id alone and is not scoped to the caller, so any authenticated caller holding a tenant_id can read that tenant; a :tenant_id that is not a valid UUID reaches the query and is handled by the shared uncaught() mapper — it matches none of the foreign-key / duplicate / not-found text patterns and therefore returns 500 InternalError rather than 400.",
        "operationId": "get_api_tenants_tenant_id",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No tenant with id <tenant_id>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/trace/exports": {
      "post": {
        "tags": [
          "sdk-trace"
        ],
        "summary": "PDF export persists trace.export row with signature",
        "description": "Signed PDF/JSON trace export. Persists trace.export with artifact_s3_key + HMAC signature; emits trace.export.requested.v1 + trace.export.ready.v1 (operational retention).",
        "operationId": "post_api_trace_exports",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "export_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "requestor_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "format": "pdf",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Required: tenant_id, requestor_persona_id, trace_id, format (pdf|json)"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — trace_id <id> not found"
          },
          "500": {
            "description": "ExportFailed — Export failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "requestor_persona_id": {
                    "type": "string"
                  },
                  "trace_id": {
                    "type": "string"
                  },
                  "format": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "requestor_persona_id": "<id from personas>",
                "trace_id": "<id from policy>",
                "format": "pdf"
              }
            }
          }
        }
      }
    },
    "/api/trace/health": {
      "get": {
        "tags": [
          "sdk-trace"
        ],
        "summary": "Health probe returns sdk + status",
        "description": "Static liveness probe for sdk-trace; returns {sdk:\"sdk-trace\", status:\"ok\"} with no DB or downstream call. Public: the gateway authGate allowlists any path ending in /health, so it answers 200 with no Authorization header, with a malformed or expired bearer token, and regardless of tenant. Edge cases: the body is a constant so there is no data path that can 404 or 500; query strings and extra headers are ignored; a non-GET verb on this path is a Fastify 404 route-miss rather than a handler error.",
        "operationId": "get_api_trace_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/trace/regression-assert": {
      "post": {
        "tags": [
          "sdk-trace"
        ],
        "summary": "Regression assert returns matched/missing/extra layers",
        "description": "FR-TRC-8 regression test API. Body: {trace_id, expected_layers: string[]}. Returns {pass, matched_layers, missing_layers, extra_layers}. CI uses this to assert traces match the expected 8-layer composition.",
        "operationId": "post_api_trace_regression_assert",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "regression_assert_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "trace_id": "{{var:trace_id}}",
                    "expected_layers": [
                      "gateway",
                      "identity",
                      "policy",
                      "meter"
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — Required: trace_id, expected_layers (string[])"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "RegressionAssertFailed — Regression assert failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "trace_id": {
                    "type": "string"
                  },
                  "expected_layers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "trace_id": "{{var:trace_id}}",
                "expected_layers": [
                  "gateway",
                  "identity",
                  "policy",
                  "meter"
                ]
              }
            }
          }
        }
      }
    },
    "/api/trace/{trace_id}": {
      "get": {
        "tags": [
          "sdk-trace"
        ],
        "summary": "Timeline renders trace header + spans",
        "description": "Trace timeline (G12). Returns trace.trace header + trace.span rows in started_at order. Render budget <5s for 50 spans.",
        "operationId": "get_api_trace_trace_id",
        "parameters": [
          {
            "name": "trace_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "trace_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "MissingPathParam — Missing path param: trace_id"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — <service message containing \"not found\">"
          },
          "500": {
            "description": "LookupFailed — Lookup failed"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/vault/decrypt": {
      "post": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Decrypt an envelope bundle produced by /api/vault/encrypt",
        "description": "Reverses POST /api/vault/encrypt: unwraps the DEK via the secret reference and decrypts the bundle, returning 200 with { plaintext_b64 }. All five of ref, ciphertext_b64, wrapped_dek_b64, iv_b64 and tag_b64 are mandatory non-empty strings - every missing field is reported in the same 400 details array. Edge cases: an unregistered or malformed ref is a 400; a tampered ciphertext, wrapped DEK, IV or auth tag fails AES-GCM authentication inside the service and is not specially mapped, so it surfaces as a generic 500 InternalError rather than a 400; decrypting a bundle whose key has since been shredded also fails as a 500; the operation is read-only and safely repeatable; requires a valid JWT.",
        "operationId": "post_api_vault_decrypt",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decrypt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ciphertext_b64": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "wrapped_dek_b64": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "iv_b64": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tag_b64": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "ciphertext_b64": {
                    "type": "string"
                  },
                  "wrapped_dek_b64": {
                    "type": "string"
                  },
                  "iv_b64": {
                    "type": "string"
                  },
                  "tag_b64": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "ref": "<id from secrets>",
                "ciphertext_b64": "<id from vault>",
                "wrapped_dek_b64": "<id from vault>",
                "iv_b64": "<id from vault>",
                "tag_b64": "<id from vault>"
              }
            }
          }
        }
      }
    },
    "/api/vault/encrypt": {
      "post": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Encrypt a base64 plaintext",
        "description": "Envelope-encrypts a base64 plaintext under the KMS key behind the supplied secret reference, returning 200 with the base64 bundle (ciphertext_b64, wrapped_dek_b64, iv_b64, tag_b64) needed by POST /api/vault/decrypt. ref is trimmed and must be non-empty; plaintext_b64 must be a non-empty string. Edge cases: an empty plaintext_b64 is rejected as missing, so encrypting empty content is not supported; a ref that is syntactically wrong or has not been registered in the secret registry is surfaced as a 400 (not a 404); the plaintext is base64-decoded leniently, so non-base64 input is not rejected outright; each call mints a fresh DEK/IV, so encrypting the same plaintext twice yields different bundles; requires a valid JWT.",
        "operationId": "post_api_vault_encrypt",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "encrypt_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "plaintext_b64": "aGVsbG8td29ybGQ=",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "plaintext_b64": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "ref": "<id from secrets>",
                "plaintext_b64": "aGVsbG8td29ybGQ="
              }
            }
          }
        }
      }
    },
    "/api/vault/health": {
      "get": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Liveness probe returns sdk + status",
        "description": "Liveness probe for sdk-vault; returns 200 with { sdk: \"sdk-vault\", status: \"ok\" }. Constant-response handler - no body parsing, no database access and no branches, so it has no failure path. The path ends in /health, so the api-gateway default-deny authGate classifies it as public: no Authorization header is required and supplying a malformed one does not produce a 401. There are therefore no error cases.",
        "operationId": "get_api_vault_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "health_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/vault/keys/{key_id}/shred": {
      "post": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Cryptographic-shred an issued key",
        "description": "Cryptographically shreds the key identified by :key_id - the key material is destroyed and everything encrypted under it becomes permanently unrecoverable. A non-empty reason in the body is mandatory and is recorded on the audit trail. Returns 200 with the shredded key record. Edge cases: a missing or empty reason is rejected with 400 before any state change; an unknown key_id and an already-shredded key both return 404 (the handler maps \"not found\" and \"already shredded\" onto the same status), so the operation is not idempotent from the caller's point of view - the first call succeeds and every replay 404s; a non-UUID key_id fails the Postgres UUID cast and is reported as a generic 500; requires a valid JWT.",
        "operationId": "post_api_vault_keys_key_id_shred",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "shred_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "compliance-shred",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — reason is required for shred"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — key not found / key already shredded"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "compliance-shred"
              }
            }
          }
        }
      }
    },
    "/api/vault/keys": {
      "post": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Issue a root-tier vault key",
        "description": "Issues a new vault key at the requested tier of the key hierarchy and returns 201 with the key record. tier, kms_ref and region are mandatory; every non-root tier additionally requires parent_key_id, which is re-checked by a database trigger. algorithm is optional and silently defaults to AES-256-GCM - any value other than ChaCha20-Poly1305 is coerced rather than rejected. Edge cases: a tier outside the canonical list is rejected with the valid values echoed back; a non-root tier without parent_key_id is rejected before the database is touched; a parent_key_id whose tier is not the legal parent of the requested tier, or which does not exist, is surfaced as a 400 from the service layer rather than a 404; kms_ref and region are trimmed, so whitespace-only values count as missing; requires a valid JWT.",
        "operationId": "post_api_vault_keys",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tier": "root",
                    "kms_ref": "kms-root-001",
                    "algorithm": "AES-256-GCM",
                    "region": "us-east-1",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tier": {
                    "type": "string"
                  },
                  "kms_ref": {
                    "type": "string"
                  },
                  "algorithm": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tier": "root",
                "kms_ref": "kms-root-001",
                "algorithm": "AES-256-GCM",
                "region": "us-east-1"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "List the calling tenant's keys",
        "description": "Lists the vault keys VISIBLE TO THE CALLING TENANT, newest first, with optional tier and scope_id narrowing and a limit capped at 500 (default 200). Returns metadata only — key_id, tier, scope_id, parent_key_id, kms_ref, state, algorithm, issued_at, rotated_at, shredded_at, tenant_id, region. There is no key material in vault.key at all, so nothing secret is returned; kms_ref is a handle, not a key. The tenant filter is applied IN SQL (listKeysForTenant), not by the handler, because every read into vault.key is a potential cross-tenant key inventory: tier + scope_id + kms_ref describe the shape of another customer's key hierarchy and parent_key_id walks upward through it. Tiers ABOVE tenant — root, app, pool — carry tenant_id NULL because they wrap every tenant, so they are excluded by construction and are NOT visible here even to the tenant that created them; an operator view goes through an ADMIN_OPS_TOKEN route instead. The definition captures NOTHING: the only key producer for this project creates a ROOT-tier key (tenant_id NULL), which this tenant-scoped list deliberately excludes, so data.0 would never populate and any consumer binding it would block forever. Edge cases: a token carrying no tenant_id is 403 rather than an empty list, because an empty list would read as 'you have no keys' when the truth is 'this token cannot express a key scope'; a fresh tenant legitimately returns an empty array with 200; tier and scope_id can only narrow within the tenant and can never widen beyond it; limit is clamped rather than rejected, so an absurd value degrades instead of failing.",
        "operationId": "get_api_vault_keys",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "no tenant scope — This token carries no tenant, so no key scope can be derived"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/vault/keys/{key_id}/rotate": {
      "post": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "Rotate a freshly-issued key",
        "description": "Rotates the key identified by :key_id, minting a new key version and returning 200 with the updated key record. The request body is optional; reason, when present, is recorded on the rotation audit trail. Edge cases: an unknown key_id and a key that exists but is not in a rotatable state (for example already shredded or retired) are collapsed into the same 404 - the handler matches on the \"not found\" / \"not in a rotatable\" substrings, so a state-precondition failure is indistinguishable from a missing key by status code alone; rotation is NOT idempotent (each successful call produces a new version); a non-UUID key_id fails the Postgres UUID cast and is reported as a generic 500; requires a valid JWT.",
        "operationId": "post_api_vault_keys_key_id_rotate",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "reason": "scheduled",
                    "rotate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — key not found / key is not in a rotatable state"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "reason": "scheduled"
              }
            }
          }
        }
      }
    },
    "/api/voice/calls": {
      "get": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "List outbound calls for the tenant",
        "description": "List the tenant's call legs, newest first, optionally filtered by status, direction, or is_voicemail. is_voicemail=true isolates the calls that answering-machine detection classified as reaching voicemail rather than a person. Edge cases: 400 when the tenant_id query param is missing; a tenant with no calls returns an empty array rather than 404; is_voicemail is only applied when the param is present (absent means 'either'), and any value other than the literal 'true' reads as false; limit defaults to 50 and offset to 0.",
        "operationId": "get_api_voice_calls",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Place a recorded call from the provisioned tracking number",
        "description": "Place an outbound call. The request always sets a statusCallback URL and, by default, requests recording plus answering-machine detection (AMD) — recording because the SOP requires it, AMD because it is what lets the status webhook tell a human answer from a voicemail. RECORDING CONSENT GATE (TK-3654): when record is requested the call is checked against sdk-consent FIRST, and recording is withheld AT SOURCE — Twilio is never asked to record — unless there is an affirmative grant. Denial and the absence of any decision both withhold (recording_withheld_reason = consent_denied / consent_unknown), so an unconfigured environment never silently records anyone; the decision is persisted on the row as recording_consent (true/false/null) with its receipt id. Response field payload.record therefore reflects what was ACTUALLY requested upstream, which may be false even when the caller sent record:true. A mirror row is written in status 'queued' before Twilio's callbacks begin arriving; the status/recording webhooks then complete it. Caller-id resolution order: explicit from_number, else the referenced tracking_number_id (must be active), else the tenant's most recently provisioned ACTIVE tracking number. Edge cases: 400 NoCallerIdAvailable when none of those resolve — the call is rejected rather than dialled from an arbitrary number; 400 when tenant_id, install_id or to_number is missing; 422 with remediation when the upstream call request fails; repeat callbacks for the same Call SID are idempotent via UNIQUE(install_id, external_id).",
        "operationId": "post_api_voice_calls",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to_number": "+14155550123",
                    "from_number": "+14155550100",
                    "tracking_number_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "initiated_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "record": true,
                    "machine_detection": true,
                    "metadata": {
                      "campaign": "google-ads-q3"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id, install_id and to_number are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "422": {
            "description": "ProviderError — call placement failed: <upstream message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "install_id": {
                    "type": "string"
                  },
                  "to_number": {
                    "type": "string"
                  },
                  "from_number": {
                    "type": "string"
                  },
                  "tracking_number_id": {
                    "type": "string"
                  },
                  "subject_persona_id": {
                    "type": "string"
                  },
                  "initiated_by_persona_id": {
                    "type": "string"
                  },
                  "record": {
                    "type": "boolean"
                  },
                  "machine_detection": {
                    "type": "boolean"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "campaign": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "install_id": "<generated uuid>",
                "to_number": "+14155550123",
                "from_number": "+14155550100",
                "tracking_number_id": "<id from voice>",
                "subject_persona_id": "<id from auth>",
                "initiated_by_persona_id": "<id from auth>",
                "record": true,
                "machine_detection": true,
                "metadata": {
                  "campaign": "google-ads-q3"
                }
              }
            }
          }
        }
      }
    },
    "/api/voice/calls/{voice_call_id}": {
      "get": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Fetch the placed call",
        "description": "Fetch a single call leg by id, including its recording fields, AMD outcome (answered_by), is_voicemail classification and any voicemail transcript. Tenant-scoped: another tenant's call reads as not-found rather than leaking its existence. Edge cases: 400 when the tenant_id query param is missing; 404 for an unknown id or one belonging to a different tenant; recording_url/recording_sid stay null until the recording webhook delivers them, and answered_by stays null until the status callback carrying AMD arrives.",
        "operationId": "get_api_voice_calls_voice_call_id",
        "parameters": [
          {
            "name": "voice_call_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "voice_call_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "404": {
            "description": "NotFound — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/voice/tracking-numbers": {
      "get": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "List active tracking numbers for the tenant",
        "description": "List the tenant's Twilio tracking numbers, newest first, optionally filtered by status. Released numbers are retained and still listed (filter status=active to exclude them) so historical call attribution survives a release. Edge cases: 400 when the tenant_id query param is missing; a tenant with no numbers returns an empty array rather than 404; limit defaults to 50 and offset to 0.",
        "operationId": "get_api_voice_tracking_numbers",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "tracking_number_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Provision a tracking number for the signup tenant",
        "description": "Provision a Twilio tracking number and pin it to the tenant so inbound calls can be attributed to the campaign/source that owns the number. The upstream purchase goes through a pluggable provider (setTwilioVoiceProvider); when no live Twilio client is injected the built-in stub mints a synthetic SID and E.164 number, so this endpoint works without Twilio credentials. Supply phone_number to claim a specific number, or area_code to let the provider pick one. Edge cases: re-provisioning the SAME upstream SID for the same install is idempotent via UNIQUE(install_id, external_id) and returns the existing row (friendly_name/purpose/assigned_persona_id are refreshed); a DIFFERENT SID resolving to a number the tenant already holds ACTIVE returns 409, because a partial unique index allows only one active claim per (tenant, phone_number); released rows are retained so historical calls stay attributable; 400 when tenant_id or install_id is missing; 422 with a remediation hint when the upstream provisioning call fails.",
        "operationId": "post_api_voice_tracking_numbers",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tracking_number_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "install_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "friendly_name": "Inbound campaign line",
                    "purpose": "google-ads-q3",
                    "area_code": "415",
                    "assigned_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and install_id are required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "409": {
            "description": "NumberAlreadyProvisioned — <number> is already provisioned and active for this tenant"
          },
          "422": {
            "description": "ProviderError — number provisioning failed: <upstream message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "install_id": {
                    "type": "string"
                  },
                  "friendly_name": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "area_code": {
                    "type": "string"
                  },
                  "assigned_persona_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "install_id": "<generated uuid>",
                "friendly_name": "Inbound campaign line",
                "purpose": "google-ads-q3",
                "area_code": "415",
                "assigned_persona_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/voice/tracking-numbers/{tracking_number_id}/release": {
      "post": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Release the provisioned tracking number",
        "description": "Release a tracking number back to Twilio and mark the mirror row 'released' with released_at stamped. The row is deliberately KEPT rather than deleted so historical calls placed from that number stay attributable, and releasing frees the (tenant, phone_number) active-claim slot so the number can be re-provisioned later. Edge cases: releasing an ALREADY-released number is an idempotent no-op that returns the current row (not a 409); 400 when tenant_id is missing from the body; 404 when the id is unknown or belongs to another tenant; 422 with remediation when the upstream release fails — retry reconciles the mirror if the number was already released in the Twilio console. After release the tenant may have no active number, in which case a later POST /api/voice/calls without an explicit from_number returns 400 NoCallerIdAvailable.",
        "operationId": "post_api_voice_tracking_numbers_tracking_number_id_release",
        "parameters": [
          {
            "name": "tracking_number_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tracking_number_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "release_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Unauthorized"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "422": {
            "description": "ProviderError — number release failed: <upstream message>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/voice/webhooks/twilio/recording": {
      "post": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Recording ready for the placed call",
        "description": "PUBLIC signed webhook: Twilio posts here once a call recording is ready, carrying RecordingSid, RecordingUrl and RecordingDuration. Authentication is by X-Twilio-Signature exactly as for the status callback (fail-closed once TWILIO_AUTH_TOKEN is set, permissive when unconfigured so local/test runs work unsigned); the gateway allowlists the /api/voice/webhooks/ prefix past the default-deny authGate. CONSENT GATE (TK-3654): recording_url is the pointer to the actual audio, so it is stored ONLY when the call carries an affirmative sdk-consent grant. A denial AND the absence of any decision both withhold it and stamp recording_withheld_reason (consent_denied / consent_unknown) — 'no recording stored without a consent decision' means missing consent must fail closed. RecordingSid and RecordingDuration ARE retained even when withheld: knowing a recording exists upstream is what makes a later deletion request actionable, and that metadata is not itself the recording. With no consent seeded (the default in a fresh environment) the happy path therefore returns recording_stored=false and recording_withheld_reason='consent_unknown'. Other edge cases: recording fields merge with COALESCE so re-delivery is idempotent and a callback missing a field never nulls a stored value; an unrecognised or absent CallSid is acknowledged with 202 rather than 404 so Twilio stops retrying; recordings normally arrive AFTER the call completed and do not alter call status; 401 InvalidSignature only when a configured signature fails to verify.",
        "operationId": "post_api_voice_webhooks_twilio_recording",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "recording_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "CallSid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "RecordingSid": "RE{{dynamic:slug}}",
                    "RecordingUrl": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                    "RecordingDuration": "40",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted — unknown CallSid <sid>"
          },
          "401": {
            "description": "InvalidSignature — InvalidSignature"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "CallSid": {
                    "type": "string"
                  },
                  "RecordingSid": {
                    "type": "string"
                  },
                  "RecordingUrl": {
                    "type": "string"
                  },
                  "RecordingDuration": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "CallSid": "<id from voice>",
                "RecordingSid": "RE{{dynamic:slug}}",
                "RecordingUrl": "https://api.twilio.com/2010-04-01/Recordings/RE{{dynamic:slug}}",
                "RecordingDuration": "40"
              }
            }
          }
        }
      }
    },
    "/api/voice/webhooks/twilio/status": {
      "post": {
        "tags": [
          "connector-twilio-voice"
        ],
        "summary": "Voicemail reached: completed call answered by a machine",
        "description": "PUBLIC signed webhook: Twilio posts call-progress here (queued -> initiated -> ringing -> in-progress -> completed, or a terminal busy/no-answer/canceled/failed). Authentication is by X-Twilio-Signature (HMAC-SHA1 over the request URL plus every parameter appended as key+value in sorted order, base64), NOT a tenant JWT — so the gateway's default-deny authGate allowlists the /api/voice/webhooks/ prefix. Verification is fail-closed once TWILIO_AUTH_TOKEN is configured and permissive when it is not, so local and test runs work unsigned. AnsweredBy carries answering-machine detection: machine_start / machine_end_* mark the call is_voicemail, while 'unknown' and 'fax' deliberately do NOT (unknown means detection was inconclusive and treating it as voicemail would log phantom voicemails). Edge cases: callbacks are RETRIED and can arrive OUT OF ORDER, so a status is never allowed to regress out of a terminal state and is_voicemail is sticky once set; an unrecognised CallSid is acknowledged with 202 rather than 404, because retrying would never make the call known; a missing CallSid is likewise 202; 401 InvalidSignature only when a token is configured and the signature is absent/wrong/over tampered params.",
        "operationId": "post_api_voice_webhooks_twilio_status",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "statu_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "CallSid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "CallStatus": "completed",
                    "AnsweredBy": "machine_start",
                    "CallDuration": "42",
                    "ErrorCode": "",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted — unknown CallSid <sid>"
          },
          "401": {
            "description": "InvalidSignature — InvalidSignature"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "CallSid": {
                    "type": "string"
                  },
                  "CallStatus": {
                    "type": "string"
                  },
                  "AnsweredBy": {
                    "type": "string"
                  },
                  "CallDuration": {
                    "type": "string"
                  },
                  "ErrorCode": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "CallSid": "<id from voice>",
                "CallStatus": "completed",
                "AnsweredBy": "machine_start",
                "CallDuration": "42",
                "ErrorCode": ""
              }
            }
          }
        }
      }
    },
    "/api/webhooks/deliveries": {
      "get": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "List DLQ for tenant",
        "description": "Lists a tenant's dead-lettered webhook deliveries — the handler delegates to listDlq, so this is the DLQ view rather than all delivery attempts. Edge cases: ?tenant_id= is REQUIRED and is caller-asserted from the query string, not the JWT, so omitting it is a 400; ?limit= is passed through Number() with no clamping, so a non-numeric limit becomes NaN and an enormous limit is honoured, while omitting it falls back to the service default of 100; a tenant with an empty DLQ returns 200 with an empty array, never 404; there is no cursor, so paging beyond the limit is impossible.",
        "operationId": "get_api_webhooks_deliveries",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "delivery_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/webhooks/deliveries/{delivery_id}/replay": {
      "post": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "Replay a single DLQ delivery",
        "description": "Replays a dead-lettered delivery by resetting it to pending with attempts=0, next_attempt_at=now() and dlq_until cleared. Takes no body. Edge cases: BOTH an unknown delivery_id AND a delivery whose status is not 'dlq' return the SAME 404 DeliveryNotInDlq — so a delivery that already succeeded or is still pending is reported as 'not in DLQ', and a SECOND replay of the same delivery also 404s because the first replay moved it out of dlq status; a delivery whose dlq_until has already passed returns 409 DlqWindowExpired, the expiry case to test; the response is the updated delivery row, not the downstream endpoint's response.",
        "operationId": "post_api_webhooks_deliveries_delivery_id_replay",
        "parameters": [
          {
            "name": "delivery_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "delivery_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "replay_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "DeliveryNotInDlq — Delivery <delivery_id> is not in DLQ"
          },
          "409": {
            "description": "DlqWindowExpired — Delivery <delivery_id> replay window has expired"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/webhooks/dlq": {
      "get": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "List DLQ deliveries for tenant",
        "description": "Gateway-composed dead-letter queue view for a tenant, delegating to sdk-webhook's listDlq with a FIXED limit of 100 — there is no ?limit= override and no paging cursor on this route, so a tenant with a deeper DLQ is silently truncated at 100 rows. Edge cases: ?tenant_id= is REQUIRED (400 if absent) and is caller-asserted from the query string rather than derived from the JWT; an empty DLQ returns 200 with an empty data array, never 404; a tenant_id that is not a valid UUID fails inside the SDK query and returns 500 with the raw Postgres message.",
        "operationId": "get_api_webhooks_dlq",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "dlq_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/webhooks/endpoints": {
      "get": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "List endpoints for tenant",
        "description": "Gateway-composed list of a tenant's registered webhook endpoints, delegating to sdk-webhook's listEndpointsForTenant. Note the POST counterpart on this same path is mounted by sdk-webhook's own router, not here. Edge cases: ?tenant_id= is REQUIRED (400 if absent) and is caller-asserted from the query string rather than derived from the JWT; a tenant with no endpoints returns 200 with an empty data array, never 404; there is no paging or limit parameter on this route; a tenant_id that is not a valid UUID fails inside the SDK query and returns 500 with the raw Postgres message.",
        "operationId": "get_api_webhooks_endpoints",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "endpoint_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — <postgres error text>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "Register HTTPS endpoint",
        "description": "Registers a tenant webhook endpoint: url, signing_key_ref and signing_algo (hmac-sha256 by default, or hmac-sha512). Edge cases: tenant_id must be a well-formed UUID and the url MUST start with https:// — plain http is rejected 400; signing_key_ref is a reference to a stored secret and its existence is NOT verified at registration time; signing_algo defaults when omitted but any other value is a 400; the registry additionally runs an SSRF-style url validator, and a rejection there throws WebhookUrlRejectedError which the controller's fail() mapper does NOT special-case — so a blocked or private URL comes back as 500 InternalError, not 400; mtls_client_cert_ref is optional and unvalidated.",
        "operationId": "post_api_webhooks_endpoints",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "endpoint_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "url": "https://webhook.example.com/projex",
                    "signing_key_ref": "{{var:signing_key_ref}}",
                    "signing_algo": "hmac-sha256",
                    "mtls_client_cert_ref": "{{var:mtls_client_cert_ref}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID / url is required / url must use https:// / signing_key_ref is required / signing_algo must be hmac-sha256 or hmac-sha512"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "signing_key_ref": {
                    "type": "string"
                  },
                  "signing_algo": {
                    "type": "string"
                  },
                  "mtls_client_cert_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "url": "https://webhook.example.com/projex",
                "signing_key_ref": "{{var:signing_key_ref}}",
                "signing_algo": "hmac-sha256",
                "mtls_client_cert_ref": "{{var:mtls_client_cert_ref}}"
              }
            }
          }
        }
      }
    },
    "/api/webhooks/endpoints/{endpoint_id}/subscribe": {
      "post": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "Subscribe to billing.invoice.finalized.v1",
        "description": "Subscribes a registered endpoint to one event_type, with an optional filter_predicate. The endpoint_id is taken from the PATH and overwrites any endpoint_id present in the body. Edge cases: :endpoint_id must be a UUID and event_type must be non-empty; the event_type must already exist in the event-type registry — an unknown one returns 400 UnregisteredEventType, a distinct code from the plain ValidationError; an endpoint_id that is a valid UUID but matches no endpoint returns 404 EndpointNotFound; filter_predicate is accepted only if it is an object and is otherwise silently dropped rather than rejected; there is no duplicate-subscription guard on the route, so re-subscribing the same (endpoint, event_type) depends on the DB constraint.",
        "operationId": "post_api_webhooks_endpoints_endpoint_id_subscribe",
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "endpoint_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "subscribe_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "event_type": "billing.invoice.finalized.v1",
                    "filter_predicate": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — endpoint_id must be a UUID / event_type is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "EndpointNotFound — Endpoint <endpoint_id> not found"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_type": {
                    "type": "string"
                  },
                  "filter_predicate": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "event_type": "billing.invoice.finalized.v1",
                "filter_predicate": {}
              }
            }
          }
        }
      }
    },
    "/api/webhooks/publish": {
      "post": {
        "tags": [
          "sdk-webhook"
        ],
        "summary": "Publish billing.invoice.finalized.v1 for tenant",
        "description": "Manually publishes an event into the webhook outbox for fan-out to every matching subscription. Returns 202 Accepted — delivery is asynchronous, so 202 means the event was ENQUEUED, not that any endpoint received it. Edge cases: tenant_id must be a UUID and event_type, event_id and a payload OBJECT are all required (a payload that is a string or array fails the typeof check); event_id is caller-supplied and acts as the outbox idempotency key, so reusing one deduplicates a repeat publish while a fresh id on retry fans the event out twice; publishing an event_type with zero subscriptions is still a successful 202 with an empty fan-out.",
        "operationId": "post_api_webhooks_publish",
        "parameters": [],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id must be a UUID / event_type is required / event_id is required / payload object is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "event_type": {
                    "type": "string"
                  },
                  "event_id": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "invoice_id": {
                        "type": "string"
                      },
                      "amount_due_cents": {
                        "type": "integer"
                      },
                      "currency": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "event_type": "billing.invoice.finalized.v1",
                "event_id": "{{var:event_id}}",
                "payload": {
                  "invoice_id": "inv_0001",
                  "amount_due_cents": 12500,
                  "currency": "USD"
                }
              }
            }
          }
        }
      }
    },
    "/api/workflows/definitions": {
      "post": {
        "tags": [
          "sdk-workflow"
        ],
        "summary": "Register a 2-step workflow using boot-registered step handlers",
        "description": "Registers or upserts a workflow definition (name, optional version/namespace, and step_specs) and returns 201 with the stored definition. Requires a valid tenant JWT (requireAuth). Edge cases: name is mandatory and step_specs must be a non-empty array whose every entry is shaped {name: string, compensate?: string} - an empty array and a malformed entry are two distinct 400 messages; registration also fails with 400 WorkflowMissingHandlers when a step named in step_specs has no runtime handler registered in this process, which means the same payload can succeed or fail depending on which handlers the deployment loaded; a step whose compensate handler is unregistered surfaces as 400 StepHandlerNotFound; because it is an upsert, re-posting the same name/version is accepted and overwrites rather than returning 409, so it is idempotent on name+version and changing step_specs silently redefines the workflow for subsequent runs.",
        "operationId": "post_api_workflows_definitions",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "definition_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "qa-workflow-demo",
                    "version": "1.0.0",
                    "namespace": "admin",
                    "step_specs": [
                      {
                        "name": "dunning.send-reminder",
                        "compensate": "dunning.rollback-reminder"
                      },
                      {
                        "name": "dunning.write-off"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — name is required / step_specs must be a non-empty array / each step_specs entry needs {name: string, compensate?: string}"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "409": {
            "description": "InvalidState — <error message containing \"not found\" or \"not in running\">"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "namespace": {
                    "type": "string"
                  },
                  "step_specs": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "compensate": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "qa-workflow-demo",
                "version": "1.0.0",
                "namespace": "admin",
                "step_specs": [
                  {
                    "name": "dunning.send-reminder",
                    "compensate": "dunning.rollback-reminder"
                  },
                  {
                    "name": "dunning.write-off"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/workflows/{run_id}": {
      "get": {
        "tags": [
          "sdk-workflow"
        ],
        "summary": "Query run state",
        "description": "Queries the full state of one workflow run by run_id - run header plus its steps and any compensations. Requires a valid tenant JWT (requireAuth). Edge cases: the lookup is by run_id alone and the caller JWT tenant is never compared to the run envelope, so any authenticated caller can read any run including its step payloads - tenant scoping must be tested explicitly; a well-formed but unknown run_id returns 404 NotFound with the id echoed in details[]; runs in every state (running, paused, completed, failed, compensated) return 200, so the caller must inspect the status field rather than expecting a non-200 for a failed run; a non-UUID run_id fails the uuid cast and is routed through fail(), whose \"not found\" matcher can turn a driver error into a 409 InvalidState, otherwise it is a 500.",
        "operationId": "get_api_workflows_run_id",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "workflow_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Run <run_id> not found"
          },
          "409": {
            "description": "InvalidState — <error message containing \"not found\" or \"not in running\">"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/workflows/{run_id}/signal": {
      "post": {
        "tags": [
          "sdk-workflow"
        ],
        "summary": "Send approval signal to a run",
        "description": "Delivers a named signal (with an optional payload) to an in-progress workflow run identified by the run_id path param, returning {signaled:true}. Requires a valid tenant JWT (requireAuth). Edge cases: signal_name is mandatory and payload defaults to {} when omitted; there is a hard state precondition - the run must be in running or paused state, so signalling a completed, failed or compensated run is a 409 InvalidState, and an unknown run_id also surfaces as a 409 rather than a 404 because fail() maps both \"not found\" and \"not in running\" onto the same status; the endpoint is not idempotent - re-sending the same signal_name delivers it again, so a retry after a timeout can double-advance the run; if the signal advances the run into a step whose handler is not registered in this process the call fails with 400 StepHandlerNotFound; the caller JWT tenant is not compared to the run envelope, so cross-tenant signalling is not blocked here.",
        "operationId": "post_api_workflows_run_id_signal",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "signal_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "signal_name": "approve",
                    "payload": {
                      "approver_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — signal_name is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "WorkflowDefinitionNotFound — Workflow definition <name> not found"
          },
          "409": {
            "description": "InvalidState — <message containing \"not found\" or \"not in running\">"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "signal_name": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object",
                    "properties": {
                      "approver_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "signal_name": "approve",
                "payload": {
                  "approver_id": "<id from auth>"
                }
              }
            }
          }
        }
      }
    },
    "/api/workflows/start": {
      "post": {
        "tags": [
          "sdk-workflow"
        ],
        "summary": "Start qa-workflow-demo run (platform-scoped)",
        "description": "Starts a run of a registered workflow definition, resolving the active definition by name (+ optional version/namespace, default namespace \"admin\"), drives the runtime engine, and returns 201 with run_id/status/steps/output. The envelope is inherited from the JWT (tenant_id/sub) when not supplied. Edge cases: missing name (only required field), a name/version/namespace with no matching active definition (404), and a step whose handler is not registered in-process (400).",
        "operationId": "post_api_workflows_start",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "start_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "qa-workflow-demo",
                    "version": "1.0.0",
                    "namespace": "admin",
                    "envelope": {
                      "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "trace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "actor": {
                        "kind": "service",
                        "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                      }
                    },
                    "input": {
                      "amount": 1500,
                      "currency": "USD"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — details[]: name is required / body must be an object"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "WorkflowDefinitionNotFound — No active workflow definition for name=... version=... namespace=..."
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "version": {
                    "type": "string"
                  },
                  "namespace": {
                    "type": "string"
                  },
                  "envelope": {
                    "type": "object",
                    "properties": {
                      "persona_id": {
                        "type": "string"
                      },
                      "trace_id": {
                        "type": "string"
                      },
                      "actor": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string"
                          },
                          "id": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "input": {
                    "type": "object",
                    "properties": {
                      "amount": {
                        "type": "integer"
                      },
                      "currency": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "qa-workflow-demo",
                "version": "1.0.0",
                "namespace": "admin",
                "envelope": {
                  "persona_id": "<id from personas>",
                  "trace_id": "<id from policy>",
                  "actor": {
                    "kind": "service",
                    "id": "<id from scim>"
                  }
                },
                "input": {
                  "amount": 1500,
                  "currency": "USD"
                }
              }
            }
          }
        }
      }
    },
    "/api/auth/verification-status": {
      "get": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Check whether an email is verified before login",
        "description": "Separate, additive read used by the UI BEFORE calling /api/auth/login to enforce email verification client-side. Takes an ?email= query param and returns { exists, verified }. exists=false for unknown emails (verified is then false). Public - the gateway authGate allowlists it (pre-login, no JWT). Never gates login itself; enforcement is the caller's.",
        "operationId": "get_api_auth_verification_status",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "verification_statu_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/auth/send-verification-email": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Send (or resend) an email-verification link",
        "description": "Separate, additive endpoint. Mints a signed email-verification token and sends the email (via the gateway send hook + platform email provider). Body accepts { email } and an optional { userId }; when userId is omitted it is resolved from the email, so a resend flow works with only the email. Returns 202 { sent, email }. Unknown email is a 404. Public - allowlisted (pre-login, no JWT). Does not alter register/signup/login.",
        "operationId": "post_api_auth_send_verification_email",
        "parameters": [],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "userId": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "userId": "{{optional}}",
                "email": "<generated email>"
              }
            }
          }
        }
      }
    },
    "/api/auth/verify-email": {
      "post": {
        "tags": [
          "sdk-identity"
        ],
        "summary": "Confirm the email-verification token from the link",
        "description": "Separate, additive endpoint. Validates the signed email-verification token (from the link in the verification email) and marks the email alias verified (sets verified_at). Body { token }. Returns 200 { verified, email }. An invalid or expired token is a 400 InvalidToken; no matching email is a 404. Public - allowlisted (pre-login, no JWT).",
        "operationId": "post_api_auth_verify_email",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "verify_email_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "token": "SIGNED_EMAIL_VERIFY_JWT",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "token": "{{static:SIGNED_EMAIL_VERIFY_JWT}}"
              }
            }
          }
        }
      }
    },
    "/api/applications": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Create a live application",
        "description": "Creates the application a credential belongs to. One per thing that calls the platform - a backend, a scheduled job, a staging copy - so a leak, a rotation and a usage figure are each scoped to one integration rather than to the whole tenant. environment is a property of the APPLICATION, not of an individual key: a test application mints pk_test_ credentials and a live one mints pk_live_, so the two can never be confused by inspection. The tenant is taken from the caller JWT and never from the payload. slug is derived from the name, is unique within the tenant, and is the client_id used by the client_credentials grant. Answers 201 with the application, or 409 when that slug is already taken in this tenant.",
        "operationId": "post_api_applications",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "application_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Journey backend",
                    "environment": "live",
                    "description": "Server-to-server calls from our backend",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "environment": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "Journey backend",
                "environment": "live",
                "description": "Server-to-server calls from our backend"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "List after creating one",
        "description": "Every application belonging to the calling tenant, newest first. Scoped from the caller claims, so there is no tenant_id parameter to get wrong and no way to enumerate the applications of another tenant. Returns 200 with applications.",
        "operationId": "get_api_applications",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "application_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/applications/{application_id}/disable": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Disable the application and revoke what it owns",
        "description": "Switches an application off and revokes every key it owns in the SAME transaction, so there is no window - and on a failure no permanent state - where an application an operator believes is off still has live credentials calling the platform. The response names the revoked key ids so the operator sees what just stopped rather than discovering it when an integration starts failing. This is the switch to reach for when a credential has leaked. Answers 200; a second call answers 404 because the application is no longer active.",
        "operationId": "post_api_applications_application_id_disable",
        "parameters": [
          {
            "name": "application_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "application_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "disable_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/applications/{application_id}": {
      "get": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Read the application just created",
        "description": "One application and every key issued under it, revoked ones included so the history stays legible. Constrained to the tenant of the caller: an application_id belonging to somebody else answers 404 rather than 403, because a 403 confirms the id is real - exactly the fact an attacker enumerating ids is trying to establish. Returns 200 with application and keys.",
        "operationId": "get_api_applications_application_id",
        "parameters": [
          {
            "name": "application_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "application_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "application_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "patch": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Rename the application just created",
        "description": "Updates an application's mutable metadata and returns 200 with data.application. ONLY name and description can be changed - every other field, including slug and status, is ignored rather than rejected, so a caller sending {slug:'new'} gets a 200 and no change. Use POST /api/applications/:application_id/disable to change status; slug is immutable because it is the client_id half of the client_credentials grant and rewriting it would silently break every integration holding a key. Both fields are optional and read only when they are strings, so a PATCH with an empty body is a valid no-op that returns the unchanged record. The tenant is taken from the verified claim via tenantOf(); an application_id belonging to another tenant is 404, not 403, so the route never confirms another tenant's applications exist. Unlike createApplicationHandler this route does NOT call tenantMismatch(), so a body tenant_id is simply ignored rather than refused.",
        "operationId": "patch_api_applications_application_id",
        "parameters": [
          {
            "name": "application_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "application_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — This credential carries no tenant context"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No such application"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "renamed-integration-{{dynamic:slug}}",
                "description": "Updated by the api_definition regression suite"
              }
            }
          }
        }
      }
    },
    "/api/applications/{application_id}/keys": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Issue a scoped, rate-limited, expiring key",
        "description": "Mints a credential under an application and returns the plaintext EXACTLY ONCE - only a keyed one-way hash and a display prefix are stored, so a lost value can only be replaced by rotating. The prefix follows the environment of the application (pk_test_ or pk_live_) rather than being chosen by the caller. scopes follow domain.resource.action and may use a tail wildcard such as sla.* so a key does not silently stop working when a new resource ships. rate_limit_rpm is enforced per key with 429 and the RateLimit headers; expires_at must be in the future. Answers 201, 404 for an application belonging to another tenant, and 409 when the application is disabled.",
        "operationId": "post_api_applications_application_id_keys",
        "parameters": [
          {
            "name": "application_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "application_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "key_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "nightly sync",
                    "scopes": [
                      "sla.clock.read",
                      "sla.clock.write"
                    ],
                    "rate_limit_rpm": 600,
                    "expires_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rate_limit_rpm": {
                    "type": "integer"
                  },
                  "expires_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "name": "nightly sync",
                "scopes": [
                  "sla.clock.read",
                  "sla.clock.write"
                ],
                "rate_limit_rpm": 600,
                "expires_at": "<generated futuredatetime+30d>"
              }
            }
          }
        }
      }
    },
    "/api/assignments": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Offer a subject with a backup and a manager",
        "description": "Opens the assignment: a primary, an optional backup, an optional manager, an acceptance window, and the SOURCE TIMESTAMP - when the WORLD produced the subject. That timestamp is required and never defaulted to now, because every SLA measures from it and quietly substituting \"now\" would restate a six-hour-old subject as fresh. It is then frozen for the life of the assignment: a decline, a reassignment, a fallback and a manager takeover all move OWNERSHIP, never the clock. QA edge cases: acceptance and response SLA clocks are started through sdk-sla FROM the source timestamp, and a clock that cannot be started leaves a null ref rather than losing the assignment; a backup equal to the primary is refused by the database; acceptance_window_minutes defaults to 5 and may be 0, which means the backup is notified at the same time. NOTE ON THE PERSONA FIELDS: primary comes from the sdk-persona producer, while the backup and the manager resolve from seeded ids. A backup or manager equal to the primary is refused (a backup that is the primary is not a backup), and the dependency graph is ONE node per METHOD+ENDPOINT so it cannot express a second and third persona from the same producer [MUST-51]; these columns are loose references with no foreign key into sdk-persona, so a seeded id is truthful rather than a fabricated FK.",
        "operationId": "post_api_assignments",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "assignment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "subject-{{dynamic:slug}}",
                    "source_timestamp": "2026-01-15T10:30:00Z",
                    "primary_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "backup_persona_id": "{{var:coverage_backup_persona_id}}",
                    "manager_persona_id": "{{var:assignment_manager_persona_id}}",
                    "acceptance_window_minutes": 5,
                    "routing_decision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor": "qa",
                    "metadata": {
                      "source": "api-test"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — source_timestamp (ISO-8601) is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "source_timestamp": {
                    "type": "string"
                  },
                  "primary_persona_id": {
                    "type": "string"
                  },
                  "backup_persona_id": {
                    "type": "string"
                  },
                  "manager_persona_id": {
                    "type": "string"
                  },
                  "acceptance_window_minutes": {
                    "type": "integer"
                  },
                  "routing_decision_id": {
                    "type": "string"
                  },
                  "actor": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "source": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_ref": "subject-{{dynamic:slug}}",
                "source_timestamp": "<generated pastdatetime>",
                "primary_persona_id": "<id from personas>",
                "backup_persona_id": "{{var:coverage_backup_persona_id}}",
                "manager_persona_id": "{{var:assignment_manager_persona_id}}",
                "acceptance_window_minutes": 5,
                "routing_decision_id": "<id from assignment>",
                "actor": "{{static:qa}}",
                "metadata": {
                  "source": "api-test"
                }
              }
            }
          }
        }
      }
    },
    "/api/assignments/{record_id}/accept": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Accept an offered assignment",
        "description": "Marks the current primary as having accepted, which stops the acceptance window. IDEMPOTENT: accepting twice returns the ORIGINAL acceptance time rather than erroring, because a retrying client is ordinary and failing the retry would leave an acceptance that happened looking like one that did not. QA edge cases: accepting an assignment that is already COMPLETED or CANCELLED is 409; the source timestamp is untouched, as it is by every other transition.",
        "operationId": "post_api_assignments_record_id_accept",
        "parameters": [
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "record_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "accept_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor": "qa",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "ASSIGNMENT_NOT_FOUND — no assignment <id>"
          },
          "409": {
            "description": "INVALID_ASSIGNMENT_TRANSITION — cannot accept an assignment that is COMPLETED"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "actor": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "actor": "{{static:qa}}"
              }
            }
          }
        }
      }
    },
    "/api/assignments/{record_id}/decline": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Decline with a reason",
        "description": "The reason is REQUIRED by the schema, not merely encouraged, and the work goes to the backup IMMEDIATELY rather than on the next sweep - waiting would spend the acceptance window twice, once on somebody who has already said no. The backup becomes the primary and a fresh acceptance window starts for them; the RESPONSE clock is not restarted, because the subject has been waiting since its source timestamp either way. QA edge cases: a blank or whitespace-only reason is 400; a decline with NO backup designated is 409 rather than leaving the subject unowned and invisible; the history records who declined, who it went to, and the reason.",
        "operationId": "post_api_assignments_record_id_decline",
        "parameters": [
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "record_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "decline_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "outside my service area",
                    "actor": "qa",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "REASON_REQUIRED — a decline must carry a reason"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "409": {
            "description": "NO_BACKUP_DESIGNATED — assignment <id> has no backup to fall to"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason": "outside my service area",
                "actor": "{{static:qa}}"
              }
            }
          }
        }
      }
    },
    "/api/assignments/{record_id}": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Read an assignment with its history",
        "description": "The assignment as it stands, plus every prior owner and why they stopped being one. The history is the answer to \"this has bounced three times, why\" - a count tells an operator nothing, while \"wrong specialty, wrong specialty, out of area\" tells them the routing rules are wrong. QA edge cases: original_persona_id is who owned it FIRST and never changes, while primary_persona_id is who owns it now; entries are sequenced and cannot be rewritten; a system fallback carries no reason, because a reason is demanded of a person who had a choice.",
        "operationId": "get_api_assignments_record_id",
        "parameters": [
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "record_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "assignment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "ASSIGNMENT_NOT_FOUND — no assignment <id>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assignments/{record_id}/reassign": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Reassign with a reason",
        "description": "Moves ownership to a named persona. The reason is required for the same reason a decline needs one: without it the history reads as noise. THE SOURCE TIMESTAMP DOES NOT MOVE - this is the invariant the whole lifecycle is built around, enforced by a database trigger rather than trusted to callers, because a reassignment that reset the clock would make a subject waiting six hours read as fresh and the breach report say all is well. QA edge cases: reassigning a COMPLETED or CANCELLED assignment is 409; the new owner gets a fresh acceptance window; original_persona_id still names whoever had it first.",
        "operationId": "post_api_assignments_record_id_reassign",
        "parameters": [
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "record_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reassign_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to_persona_id": "{{var:coverage_backup_persona_id}}",
                    "reason": "territory rebalance",
                    "actor": "qa-manager",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — to_persona_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "to_persona_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "to_persona_id": "{{var:coverage_backup_persona_id}}",
                "reason": "territory rebalance",
                "actor": "{{static:qa-manager}}"
              }
            }
          }
        }
      }
    },
    "/api/assignments/sweep": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Sweep expired offers",
        "description": "Scans offers whose acceptance window has run out and moves each to its backup. Returns what moved AND what could not: an offer that expired with NO backup is reported as STRANDED rather than skipped, because that is the case most in need of a human and a sweep that counted it as \"nothing to do\" is how a subject sits unowned for a day. QA edge cases: safe to run repeatedly - an offer already accepted or already moved is re-checked under a row lock and left alone, so a concurrent accept wins; system fallbacks are recorded in the history with actor system:acceptance-window and no reason; limit is clamped to 1000.",
        "operationId": "post_api_assignments_sweep",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "sweep_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "limit": 100,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "limit": 100
              }
            }
          }
        }
      }
    },
    "/api/assignment/decisions": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Read one recorded decision by id",
        "description": "Reads decisions back: one by decision_id, or a filtered list by subject or outcome. Filtering on REVIEW is how an operator works the queue of subjects the pipeline deliberately refused to guess about. QA edge cases: the trace returned is the one recorded AT THE TIME, including the rule version - it is not recomputed, so it stays valid after the rules change; a decision_id belonging to another tenant is 404 rather than 403, because confirming an id exists elsewhere is itself a leak; the list is newest-first and limit is clamped to 500.",
        "operationId": "get_api_assignment_decisions",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "decision_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "ROUTING_DECISION_NOT_FOUND — no decision <id>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assignment/rotation": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Read the rotation cursors",
        "description": "Reads EP-335's rotation cursors without advancing them. The read-only part is the point: advancing a cursor from a GET would skew the real rotation for everybody who merely looked at it, and a dashboard polling this endpoint would silently starve whoever came next in the rota. QA edge cases: a tenant with no rotation yet gets an empty array, not a 404 - the cursor is created on first use by the assignment engine; filtering by pool_key or strategy narrows the list.",
        "operationId": "get_api_assignment_rotation",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "rotation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assignment/route": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Route a subject through the pipeline",
        "description": "Runs the six-step pipeline - eligibility, priority, specialty, availability, assignment, fallback - and returns the chosen persona WITH a per-step trace in plain language. The trace is the product: an operator asking \"why did this go there\" months later needs the explanation that was written down at the time, because re-running the pipeline today answers a different question once the rules and everybody's availability have moved on. QA edge cases: an outcome of REVIEW is a SUCCESSFUL answer (200), not an error - \"this needs a human\" is a decision; a subject that cannot ANSWER an eligibility predicate (it lacks the field the rule reads) goes to REVIEW rather than being force-assigned, and is deliberately distinct from UNROUTABLE, which means the rule was answered and the answer was no; if no coverage resolver is wired the availability step also returns REVIEW rather than assuming everybody is free; dry_run=true returns the same trace without recording the decision.",
        "operationId": "post_api_assignment_route",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "subject-{{dynamic:slug}}",
                    "subject": {
                      "region": "TX",
                      "severity": 9,
                      "required_specialty": "roofing"
                    },
                    "candidate_persona_ids": [
                      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    ],
                    "persona_specialties": {},
                    "rule_set_name": "default",
                    "dry_run": false,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — subject_ref is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "object",
                    "properties": {
                      "region": {
                        "type": "string"
                      },
                      "severity": {
                        "type": "integer"
                      },
                      "required_specialty": {
                        "type": "string"
                      }
                    }
                  },
                  "candidate_persona_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "persona_specialties": {
                    "type": "object",
                    "properties": {}
                  },
                  "rule_set_name": {
                    "type": "string"
                  },
                  "dry_run": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_ref": "subject-{{dynamic:slug}}",
                "subject": {
                  "region": "TX",
                  "severity": 9,
                  "required_specialty": "roofing"
                },
                "candidate_persona_ids": [
                  "<id from personas>"
                ],
                "persona_specialties": {},
                "rule_set_name": "default",
                "dry_run": false
              }
            }
          }
        }
      }
    },
    "/api/assignment/routes": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "List rule versions",
        "description": "Lists every published version of a named rule set, newest first, with exactly one marked active. This is the audit view: which rules are in force, what they replaced, and what a rollback would return to. QA edge cases: versions are never deleted, so the list only grows - a decision from months ago still points at a version that is here; a tenant that has published nothing gets an empty array rather than a 404, because \"no rules yet\" is a normal state and routing simply treats every subject as routable.",
        "operationId": "get_api_assignment_routes",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Publish a rule set and make it active",
        "description": "Routing rules are DATA, not code: publishing a version is this call, and switching which one is in force is the same call with activate_version. A routing rule changes when the business changes, which is weekly; a deploy is not weekly, so rules that live in code get hard-coded into one vertical and the platform stops being one. A published version is FROZEN - it can never be edited, only superseded - because every decision names the version that produced it, and an editable version would explain last month's decision with this month's rules, which is worse than no explanation because it is a confident wrong one. QA edge cases: sending activate_version switches the active version and returns 200 (nothing was created); sending rules publishes a NEW version and returns 201, with the version number allocated server-side (never supplied by the caller); exactly one version per named set is active at a time, so activating one deactivates the previous automatically; activating a version that does not exist is 404; rolling back is simply activating an earlier version number.",
        "operationId": "post_api_assignment_routes",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "route_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "default-{{dynamic:slug}}",
                    "rules": {
                      "eligibility": [
                        {
                          "field": "region",
                          "op": "present",
                          "because": "a subject with no region cannot be routed"
                        }
                      ],
                      "priority_bands": [
                        {
                          "band": "urgent",
                          "when": [
                            {
                              "field": "severity",
                              "op": "gte",
                              "value": 8
                            }
                          ]
                        },
                        {
                          "band": "standard",
                          "when": []
                        }
                      ],
                      "specialty": {
                        "field": "required_specialty"
                      },
                      "assignment": {
                        "pick": "most_headroom"
                      },
                      "fallback": {
                        "to_review": true
                      }
                    },
                    "activate": true,
                    "published_by": "qa",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — rules (object) or activate_version (number) is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "ROUTING_RULE_SET_NOT_FOUND — no active routing rule set 'x' for this tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "rules": {
                    "type": "object",
                    "properties": {
                      "eligibility": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "op": {
                              "type": "string"
                            },
                            "because": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "priority_bands": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "band": {
                              "type": "string"
                            },
                            "when": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "field": {
                                    "type": "string"
                                  },
                                  "op": {
                                    "type": "string"
                                  },
                                  "value": {
                                    "type": "integer"
                                  }
                                }
                              }
                            }
                          }
                        }
                      },
                      "specialty": {
                        "type": "object",
                        "properties": {
                          "field": {
                            "type": "string"
                          }
                        }
                      },
                      "assignment": {
                        "type": "object",
                        "properties": {
                          "pick": {
                            "type": "string"
                          }
                        }
                      },
                      "fallback": {
                        "type": "object",
                        "properties": {
                          "to_review": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  },
                  "activate": {
                    "type": "boolean"
                  },
                  "published_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "default-{{dynamic:slug}}",
                "rules": {
                  "eligibility": [
                    {
                      "field": "region",
                      "op": "present",
                      "because": "a subject with no region cannot be routed"
                    }
                  ],
                  "priority_bands": [
                    {
                      "band": "urgent",
                      "when": [
                        {
                          "field": "severity",
                          "op": "gte",
                          "value": 8
                        }
                      ]
                    },
                    {
                      "band": "standard",
                      "when": []
                    }
                  ],
                  "specialty": {
                    "field": "required_specialty"
                  },
                  "assignment": {
                    "pick": "most_headroom"
                  },
                  "fallback": {
                    "to_review": true
                  }
                },
                "activate": true,
                "published_by": "{{static:qa}}"
              }
            }
          }
        }
      }
    },
    "/api/assignment/simulate": {
      "post": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Simulate a candidate rule version",
        "description": "Replays recorded decisions through a candidate rule VERSION and reports what would have changed: per-persona actual versus candidate, the outcome mix, the subjects whose destination moves, and a skew audit naming both over-allocation and starvation. SIDE EFFECT FREE, and it proves it rather than claiming it - the response carries a side_effects block that must be all zeros (assignments, notifications, clocks, and decisions written, counted from the table before and after). Returns 200, not 201, because nothing was created. QA edge cases: the candidate is evaluated BY VERSION and is never activated - activating a rule set to try it out is the experiment changing production; the EP-335 rotation cursor is read but never advanced; history is replayed as the rules SAW it (the trace records the fields each rule read), so a candidate rule reading a field the history never carried comes out AMBIGUOUS and goes to review rather than being silently defaulted.",
        "operationId": "post_api_assignment_simulate",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "simulate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "candidate_version": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rule_set_name": "default",
                    "candidate_persona_ids": [
                      "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    ],
                    "persona_specialties": {},
                    "limit": 200,
                    "skew_tolerance": 0.5,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — candidate_version (number) is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "candidate_version": {
                    "type": "string"
                  },
                  "rule_set_name": {
                    "type": "string"
                  },
                  "candidate_persona_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "persona_specialties": {
                    "type": "object",
                    "properties": {}
                  },
                  "limit": {
                    "type": "integer"
                  },
                  "skew_tolerance": {
                    "type": "number"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "candidate_version": "<id from assignment>",
                "rule_set_name": "default",
                "candidate_persona_ids": [
                  "<id from personas>"
                ],
                "persona_specialties": {},
                "limit": 200,
                "skew_tolerance": 0.5
              }
            }
          }
        }
      }
    },
    "/api/assignment/workload/{persona_id}": {
      "put": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "Upsert a persona workload profile",
        "description": "Upserts a persona's workload profile (capacity/day, skills, availability window) through the existing setWorkload service (FR-ASN-3) — the HTTP producer surface so tests and dispatchers provision assignment candidates via the API instead of a SQL seed. Idempotent on persona_id (ON CONFLICT DO UPDATE; capacity/skills COALESCE to prior value when omitted). open_tasks is dispatcher-owned and is NOT settable here. Edge cases: persona_id must be a UUID (non-UUID -> 400 ValidationError before the ::uuid cast); a valid tenant JWT is required (api-gateway default-deny authGate). NOTE: assignment.workload has no tenant column, so this is effectively a global/ops surface.",
        "operationId": "put_api_assignment_workload_persona_id",
        "parameters": [
          {
            "name": "persona_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "persona_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "workload_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capacity_per_day": 8,
                    "skills": [
                      "plumbing",
                      "hvac"
                    ],
                    "available_from": "2026-01-01T00:00:00Z",
                    "available_to": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — persona_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "capacity_per_day": {
                    "type": "integer"
                  },
                  "skills": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "available_from": {
                    "type": "string"
                  },
                  "available_to": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "capacity_per_day": 8,
                "skills": [
                  "plumbing",
                  "hvac"
                ],
                "available_from": "2026-01-01T00:00:00Z",
                "available_to": "<generated futuredatetime+30d>"
              }
            }
          }
        }
      }
    },
    "/api/auth/token": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Exchange a key for a service token",
        "description": "RFC 6749 section 4.4. Exchanges an application credential (client_id is the application slug or id, client_secret is the pk_live_ or pk_test_ key) for a short-lived service JWT carrying the scopes, tenant and synthetic persona of the key, with actor.kind service so machine traffic stays distinguishable from a human in the audit trail. Public by necessity - the credential IS the request body - and never cached (Cache-Control no-store). The optional scope parameter may NARROW what the key holds and can never widen it. Invalid, revoked and expired credentials all answer invalid_client with identical wording, so probing cannot confirm that a client exists. Because a minted token cannot be revoked before it expires, its lifetime is the revocation delay and is capped at one hour.",
        "operationId": "post_api_auth_token",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "token_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "grant_type": "client_credentials",
                    "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "client_secret": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string"
                  },
                  "client_id": {
                    "type": "string"
                  },
                  "client_secret": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "grant_type": "client_credentials",
                "client_id": "<id from applications>",
                "client_secret": "<id from applications>"
              }
            }
          }
        }
      }
    },
    "/api/config": {
      "get": {
        "tags": [
          "sdk-config"
        ],
        "summary": "List tenant-scoped config",
        "description": "Lists active config rows for a scope. scope query param is required; scope_id defaults to the caller's tenant for tenant scope. Tenant JWT required. Edge cases: scope must be platform|tenant|app|app_user (400).",
        "operationId": "get_api_config",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — scope must be platform|tenant|app|app_user"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-config"
        ],
        "summary": "Set a tenant-scoped config value",
        "description": "Upserts a config value in the multi-scope config plane (EP-341) on the (scope, scope_id, key) unique key. A non-secret value goes inline in `value`; a secret keeps only its sdk-secrets envelope pointer in `secret_ref` (value XOR secret_ref). Tenant JWT required; a tenant may only write its OWN tenant/app/app_user rows (403 otherwise) and platform scope requires a platform operator. Edge cases: scope must be platform|tenant|app|app_user and key is required (400); providing both value and secret_ref is 400.",
        "operationId": "post_api_config",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "config_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "scope": "tenant",
                    "scope_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "key": "qa.config.smoke",
                    "value": {
                      "provider": "anthropic",
                      "model": "claude-opus-4-8"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — scope (platform|tenant|app|app_user) and key are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — cannot write another tenant's config"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "string"
                  },
                  "scope_id": {
                    "type": "string"
                  },
                  "key": {
                    "type": "string"
                  },
                  "value": {
                    "type": "object",
                    "properties": {
                      "provider": {
                        "type": "string"
                      },
                      "model": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "scope": "tenant",
                "scope_id": "<id from auth>",
                "key": "qa.config.smoke",
                "value": {
                  "provider": "anthropic",
                  "model": "claude-opus-4-8"
                }
              }
            }
          }
        }
      }
    },
    "/api/config/resolve": {
      "get": {
        "tags": [
          "sdk-config"
        ],
        "summary": "Resolve a key set at tenant scope",
        "description": "Resolves the MOST-specific active value for a key in the caller's context, walking app_user -> app -> tenant -> platform (first active wins). tenant comes from the JWT; app_id/app_user_id may be overridden via query. Returns {data:{key, resolved:{scope,scope_id,value,secret_ref}|null}} — resolved is null when no scope in the chain has an active value. Edge cases: key query param is required (400); tenant JWT required (401).",
        "operationId": "get_api_config_resolve",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "resolve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — key is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/config/revoke": {
      "post": {
        "tags": [
          "sdk-config"
        ],
        "summary": "Revoke a tenant config value",
        "description": "Soft-deletes a config value (status='revoked') so resolution stops without losing the audit row. Operates on its OWN seeded key so it never revokes the read tests' value. Tenant JWT required + scope-ownership guard. Edge cases: scope + key required (400); unknown row 404; cross-tenant/platform without operator 403.",
        "operationId": "post_api_config_revoke",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "scope": "tenant",
                    "scope_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "key": "qa.config.revoke.smoke",
                    "revoke_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — scope and key are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — config value not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "string"
                  },
                  "scope_id": {
                    "type": "string"
                  },
                  "key": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "scope": "tenant",
                "scope_id": "<id from auth>",
                "key": "qa.config.revoke.smoke"
              }
            }
          }
        }
      }
    },
    "/api/config/rotate": {
      "post": {
        "tags": [
          "sdk-config"
        ],
        "summary": "Rotate a tenant secret config value",
        "description": "Rotates a secret value's sdk-secrets envelope pointer in place (secret_ref swapped, value cleared, re-activated). Operates on its OWN seeded secret key. Tenant JWT required + scope-ownership guard. Edge cases: scope + key + secret_ref required (400); unknown row 404.",
        "operationId": "post_api_config_rotate",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "scope": "tenant",
                    "scope_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "key": "qa.config.rotate.smoke",
                    "secret_ref": "vault:rotated-{{dynamic:slug}}",
                    "rotate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — scope, key and secret_ref are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — config value not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "string"
                  },
                  "scope_id": {
                    "type": "string"
                  },
                  "key": {
                    "type": "string"
                  },
                  "secret_ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "scope": "tenant",
                "scope_id": "<id from auth>",
                "key": "qa.config.rotate.smoke",
                "secret_ref": "vault:rotated-{{dynamic:slug}}"
              }
            }
          }
        }
      }
    },
    "/api/config/value": {
      "get": {
        "tags": [
          "sdk-config"
        ],
        "summary": "Get one tenant config row",
        "description": "Returns one exact config row by (scope, scope_id, key). Tenant JWT required. Edge cases: scope + key required (400); unknown row is 404.",
        "operationId": "get_api_config_value",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "value_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — scope and key are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — config value not found"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/lead-forms/{tenant_id}": {
      "get": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "List all archived deliveries for the tenant",
        "description": "Lists archived lead-form deliveries for a tenant, filterable by platform and outcome (accepted | rejected | duplicate). Proves AC4 — the triage view exists, and every row reports has_raw, so an operator can confirm the payload is still recoverable before attempting a re-process. Unlike the ingest endpoint this IS tenant-authed: the ingest caller is a platform proving itself with an HMAC, whereas reading a tenant's archived lead payloads is ordinary tenant data access and the default-deny gate applies (MUST-52). QA edge cases: rejected deliveries are the primary reason to call this, so outcome=rejected is the interesting filter and its rows always carry has_raw true — a flag with no reader would make 'archived on rejection' meaningless; the raw payload itself is deliberately NOT returned in the list, since these carry customer PII and a list endpoint should not spray it, so the caller fetches or re-processes a specific event instead; limit is clamped to 1..500; results are newest-first; an unknown platform filter simply matches nothing rather than erroring, since the filter is a narrowing rather than an assertion.",
        "operationId": "get_api_connectors_lead_forms_tenant_id",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "lead_form_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/lead-forms/{tenant_id}/{platform}": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Meta lead form with valid signature is accepted and archived",
        "description": "Receives a completed lead form from Meta/Instagram/Facebook, LinkedIn Lead Gen, TikTok or Google/YouTube. requiresAuth is FALSE by design and correctly so (MUST-52's signed-webhook exemption): the caller is a platform, not a tenant session, and the provider HMAC is the trust boundary. Proves all four criteria. The ORDER of the four steps is the whole design: verify signature, then reserve-and-archive in one INSERT, then normalise. AC2: an unsigned or wrongly-signed delivery is rejected 401 and stores NOTHING — it is not a rejected lead, it is not a lead at all, and archiving it would let anyone fill the tenant's table with material to triage; comparison is constant-time, since a plain === on an HMAC leaks the first wrong byte through timing. AC3: idempotency is the UNIQUE index on (tenant_id, platform, source_event_id) plus ON CONFLICT DO NOTHING — the insert IS the replay check, which is correct even when the provider delivers to several workers at once, where a read-then-write check would let two of them each create a lead from one form submission. AC4: the raw payload is written BEFORE normalisation is allowed an opinion, so a rejected mapping keeps the evidence and the row can be re-processed later; the lead form is the only record the person filled it in and the platform will not re-send it. AC1: each adapter extracts its own full field set — Meta (form + version, campaign/ad/creative, DM thread and comment id, permission fields), LinkedIn (campaign/form ids, member and company URNs, message thread), TikTok (ttclid, advertiser, page), Google (gclid, UTM, form proof) — and permission evidence is a REQUIRED field whose absence fails normalisation, because a lead form is a consent artefact and a lead with no recorded consent has no lawful basis for contact. QA edge cases: the payload is archived twice on purpose — raw_body as the exact signed BYTES (an HMAC is over bytes, so only this can re-verify the signature months later) and raw_payload as jsonb for querying, because jsonb reorders keys and could never round-trip a signature; Google additionally requires a form proof distinct from the transport signature, since the transport proves the request came from Google while the form proof proves the lead came from THIS advertiser's form — accepting one as the other would allow cross-account lead injection; a Google lead with neither email nor phone is refused as uncontactable; 202 is returned for everything past the trust boundary INCLUDING a rejected normalisation and a replay, because a 4xx there makes the provider retry forever against a payload already held; an unknown platform is 404.",
        "operationId": "post_api_connectors_lead_forms_tenant_id_platform",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          },
          {
            "name": "platform",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "platform path parameter"
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "INVALID_SIGNATURE — invalid or missing x-hub-signature-256 signature"
          },
          "404": {
            "description": "UNKNOWN_PLATFORM — platform must be one of: META, LINKEDIN, TIKTOK, GOOGLE"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entry": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "changes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "value": {
                                "type": "object",
                                "properties": {
                                  "leadgen_id": {
                                    "type": "string"
                                  },
                                  "form_id": {
                                    "type": "string"
                                  },
                                  "form_version": {
                                    "type": "string"
                                  },
                                  "campaign_id": {
                                    "type": "string"
                                  },
                                  "ad_id": {
                                    "type": "string"
                                  },
                                  "creative_id": {
                                    "type": "string"
                                  },
                                  "created_time": {
                                    "type": "string"
                                  },
                                  "platform": {
                                    "type": "string"
                                  },
                                  "thread_id": {
                                    "type": "string"
                                  },
                                  "comment_id": {
                                    "type": "string"
                                  },
                                  "consent": {
                                    "type": "object",
                                    "properties": {
                                      "consent_ref": {
                                        "type": "string"
                                      },
                                      "granted": {
                                        "type": "boolean"
                                      }
                                    }
                                  },
                                  "permission_fields": {
                                    "type": "array",
                                    "items": {
                                      "type": "string"
                                    }
                                  },
                                  "field_data": {
                                    "type": "array",
                                    "items": {
                                      "type": "object",
                                      "properties": {
                                        "name": {
                                          "type": "string"
                                        },
                                        "values": {
                                          "type": "array",
                                          "items": {
                                            "type": "string"
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "entry": [
                  {
                    "id": "page-1",
                    "changes": [
                      {
                        "value": {
                          "leadgen_id": "lead-1",
                          "form_id": "form-9",
                          "form_version": "v2",
                          "campaign_id": "camp-1",
                          "ad_id": "ad-2",
                          "creative_id": "cr-3",
                          "created_time": "2026-08-01T09:00:00Z",
                          "platform": "instagram",
                          "thread_id": "dm-77",
                          "comment_id": "cm-88",
                          "consent": {
                            "consent_ref": "consent-abc",
                            "granted": true
                          },
                          "permission_fields": [
                            "marketing_opt_in"
                          ],
                          "field_data": [
                            {
                              "name": "email",
                              "values": [
                                "jane@acme.test"
                              ]
                            }
                          ]
                        }
                      }
                    ]
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/connectors/lead-forms/{tenant_id}/events/{event_id}/reprocess": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Re-process a previously rejected delivery",
        "description": "Re-runs normalisation over an ALREADY ARCHIVED delivery. An action endpoint, so 200. This is the payoff for archiving raw on rejection (AC4) and the reason that criterion is worth having: a mapping fixed today can recover leads rejected last week, which is simply impossible if the payload was discarded when normalisation first refused it — and the platform will not re-send a lead form. Re-processing reads the stored raw_payload, runs the current adapter, and flips the row to accepted (clearing rejection_reason) or re-records a new rejection reason. QA edge cases: the signature is NOT re-checked here, correctly — it was verified at ingest and the row would not exist otherwise, and re-verifying would require the original per-tenant secret which may since have rotated (the byte-exact raw_body column is retained separately so that verification remains POSSIBLE for audit, just not required for re-processing); re-processing an already-accepted event is harmless and idempotent, since it recomputes the same normalisation from the same bytes; an event_id belonging to a different tenant is 404 rather than 403, so the endpoint does not confirm the id exists to someone who should not know; requiresAuth applies — this mutates tenant data.",
        "operationId": "post_api_connectors_lead_forms_tenant_id_events_event_id_reprocess",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          },
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "event_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reprocess_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "LEAD_FORM_EVENT_NOT_FOUND — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/connectors/lead-forms/{tenant_id}/website": {
      "post": {
        "tags": [
          "sdk-connectors"
        ],
        "summary": "Chat lead with transcript and human handoff",
        "description": "Receives demo-request, pricing-enquiry, contact and chat events from the tenant's own site and chat widget, on the SAME endpoint and the same contract as the paid-social adapters — platform 'website'. Sharing the pipeline is deliberate: a first-party form is the same KIND of thing as a paid-social lead (a completed intent signal with consent attached), and a parallel pipeline would mean a second place for the archive-first ordering and the replay guarantee to be got subtly wrong. requiresAuth is FALSE under MUST-52's signed-webhook exemption; the HMAC over the exact bytes is the trust boundary. AC1: the transcript is captured turn by turn with role and timestamp, and the handoff block records state, who took over, when and why — for a chat lead the conversation IS the qualifying information and the visitor will not repeat it, while the handoff state is what decides whether anyone is already talking to this person (get it wrong and either two reps reply or nobody does); a chat event carrying NO transcript is refused rather than half-accepted, since that is almost always an integration bug that would otherwise produce a lead nobody can qualify. AC2: the submitted permission block is stored VERBATIM in permission.submitted_raw — exact keys, casing and values, including the consent wording the person actually saw — because interpreting consent into a tidy shape loses the wording, and the wording is what a regulator asks to see; the normalised granted/consent_ref/scopes view is derived alongside it for code. AC3 and AC4 are inherited unchanged from the social contract: idempotency is the UNIQUE (tenant_id, platform, source_event_id) index with ON CONFLICT DO NOTHING, correct under concurrent delivery where a read-then-write check is not, and the raw event is archived BEFORE normalisation is allowed an opinion, in both a byte-exact raw_body column (so the signature stays re-verifiable) and queryable jsonb. QA edge cases: page_url, page_title, referrer and session_id are captured as context and UTM/gclid as attribution; an unknown event_kind is refused rather than guessed; a permission block present but with nothing granted is refused; a non-chat form event needs no transcript and defaults handoff.state to 'none'; the idempotency key falls back event_id then submission_id then session_id.",
        "operationId": "post_api_connectors_lead_forms_tenant_id_website",
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "tenant_id path parameter"
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "accepted",
                    "job_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "401": {
            "description": "INVALID_SIGNATURE — invalid or missing x-projex-signature signature"
          }
        },
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event_id": {
                    "type": "string"
                  },
                  "event_kind": {
                    "type": "string"
                  },
                  "session_id": {
                    "type": "string"
                  },
                  "page_url": {
                    "type": "string"
                  },
                  "page_title": {
                    "type": "string"
                  },
                  "referrer": {
                    "type": "string"
                  },
                  "form_id": {
                    "type": "string"
                  },
                  "form_version": {
                    "type": "string"
                  },
                  "submitted_at": {
                    "type": "string"
                  },
                  "utm_source": {
                    "type": "string"
                  },
                  "utm_medium": {
                    "type": "string"
                  },
                  "utm_campaign": {
                    "type": "string"
                  },
                  "fields": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string"
                      },
                      "company": {
                        "type": "string"
                      }
                    }
                  },
                  "permissions": {
                    "type": "object",
                    "properties": {
                      "Marketing_Opt_In": {
                        "type": "string"
                      },
                      "consent_ref": {
                        "type": "string"
                      },
                      "wording": {
                        "type": "string"
                      }
                    }
                  },
                  "transcript": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string"
                        },
                        "text": {
                          "type": "string"
                        },
                        "at": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "handoff": {
                    "type": "object",
                    "properties": {
                      "state": {
                        "type": "string"
                      },
                      "handed_to": {
                        "type": "string"
                      },
                      "handed_at": {
                        "type": "string"
                      },
                      "reason": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "event_id": "web-1",
                "event_kind": "chat",
                "session_id": "sess-1",
                "page_url": "https://acme.test/pricing",
                "page_title": "Pricing",
                "referrer": "https://google.test/search?q=crm",
                "form_id": "chat-widget-v3",
                "form_version": "3.1",
                "submitted_at": "2026-08-01T09:00:00Z",
                "utm_source": "google",
                "utm_medium": "cpc",
                "utm_campaign": "brand",
                "fields": {
                  "email": "jane@acme.test",
                  "company": "Acme Ltd"
                },
                "permissions": {
                  "Marketing_Opt_In": "true",
                  "consent_ref": "web-consent-1",
                  "wording": "I agree to be contacted about my enquiry"
                },
                "transcript": [
                  {
                    "role": "visitor",
                    "text": "Do you support SSO?",
                    "at": "2026-08-01T08:58:00Z"
                  },
                  {
                    "role": "bot",
                    "text": "Yes, SAML and OIDC.",
                    "at": "2026-08-01T08:58:10Z"
                  },
                  {
                    "role": "visitor",
                    "text": "Can I talk to sales?",
                    "at": "2026-08-01T08:59:00Z"
                  }
                ],
                "handoff": {
                  "state": "human",
                  "handed_to": "agent-7",
                  "handed_at": "2026-08-01T08:59:30Z",
                  "reason": "visitor asked for sales"
                }
              }
            }
          }
        }
      }
    },
    "/api/conversations/compose-guardrail": {
      "post": {
        "tags": [
          "sdk-conversation"
        ],
        "summary": "Opt-out outranks quiet hours in the reason list",
        "description": "Evaluates whether the tenant may compose on each requested channel, returning allow|review|deny per channel plus an ORDERED, human-readable reason list — never a bare boolean (AC1). It is an action endpoint, not a create, so the success status is 200. The ordering is the contract: reasons a human cannot resolve by waiting come first (LEGAL_HOLD, OPTED_OUT, SUPPRESSED, MISSING_CONSENT, NO_SENDER_IDENTITY) ahead of ones that clear on their own (FREQUENCY_CAP, RATE_LIMITED, QUIET_HOURS), so reasons[0] is the thing actually worth telling the user; every runner-up is retained because clearing only the headline would send the caller round the loop twice. Decision inputs come entirely from the caller (AC2/AC3): over HTTP a resolver function cannot be passed, so the caller sends channel_facts — the output ITS resolver produced — and this endpoint only ranks and explains. The SDK holds no consent table, no quiet-hours calendar, no DNC list and no policy constants, so a vertical changes its rules by changing its resolver, never by forking this package. QA edge cases: a channel the caller says nothing about resolves to no facts, hence no reasons, hence allow — silence must not become an invented denial, which would be this package holding an opinion about consent; available and has_sender_identity are the only flags read as 'false is bad', and undefined is deliberately NOT the same as false; identical facts must yield an identical verdict on every customer-facing channel, since any difference would betray a hard-coded assumption about that channel's regime; INTERNAL_NOTE is never passed to the resolver at all (a note to a colleague is not contact with the customer) and is never returned as recommended_channel; a closed thread yields review rather than deny because reopening is a decision a human can make; recommended_channel is the FIRST allowed channel in the caller's own submitted order, since they listed them in preference order and re-ordering would itself be policy. 401 without a tenant Bearer; 400 for an empty channels array, an unknown channel, or a missing channel_facts.",
        "operationId": "post_api_conversations_compose_guardrail",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "compose_guardrail_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channels": [
                      "SMS"
                    ],
                    "channel_facts": {
                      "SMS": {
                        "opted_out": true,
                        "quiet_hours": true
                      }
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — channels must be a non-empty array"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "channel_facts": {
                    "type": "object",
                    "properties": {
                      "SMS": {
                        "type": "object",
                        "properties": {
                          "opted_out": {
                            "type": "boolean"
                          },
                          "quiet_hours": {
                            "type": "boolean"
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channels": [
                  "SMS"
                ],
                "channel_facts": {
                  "SMS": {
                    "opted_out": true,
                    "quiet_hours": true
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/conversations/inbox": {
      "get": {
        "tags": [
          "sdk-conversation"
        ],
        "summary": "Unfiltered inbox returns the tenant's open threads",
        "description": "The agent inbox: threads for a tenant, most recently active first, with three independent filters that AND together — unread=true (at least one unread INBOUND message), awaiting_reply=true (we spoke last and are waiting on them) and channel=<CHANNEL>. Proves AC4. Reads the trigger-maintained rollup columns (unread_count, status, channel_set) rather than joining messages, which is why those columns exist; channel_set is GIN-indexed. The channel filter is 'has this thread EVER carried that channel' (channel_set @> ARRAY[channel]), not 'the latest message was that channel' — the latter would make a conversation vanish from the SMS view the moment somebody replied by email, which is the opposite of what an omnichannel inbox is for. QA edge cases: only the literal string 'true' enables a flag, so ?unread=false correctly means 'do not filter' rather than silently becoming unread-only (any-truthy parsing is the bug this guards against); closed threads are excluded unless include_closed=true; an unknown channel value is a 400 rather than a silently empty list, since an empty 200 is indistinguishable from 'no matches'; limit is clamped to 1..200 and offset floored at 0, so absurd paging cannot be used to pull the whole table; the response echoes the filters it actually applied so a caller can tell an empty inbox from a mis-parsed query. requiresAuth applies to this GET exactly as to the POSTs (MUST-52) — tenant_id is a query param but the Bearer is what scopes the read.",
        "operationId": "get_api_conversations_inbox",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "inbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/conversations/messages": {
      "post": {
        "tags": [
          "sdk-conversation"
        ],
        "summary": "Record an outbound SMS on the thread",
        "description": "Records one message onto an existing thread. Collection-root create, so 201 with { data: { message } }. body_ref is a vault/blob REFERENCE, never the body text: message content is customer data and a text column here would put it in every backup, replica and query plan that touches the table. occurred_at is when the provider says it HAPPENED and is the sort key; received_at is stamped server-side and is a diagnostic only, so a message delivered late still renders in the right place. Writes carrying external_message_id are idempotent on (tenant_id, channel, external_message_id) — a retried provider webhook returns the row it already wrote instead of double-posting into the thread. INTERNAL_NOTE (or direction INTERNAL) is routed internally to addInternalNote, which hard-codes the undispatchable shape; the ordinary path is refused for notes because it accepts a caller-supplied delivery_state and that is exactly the field a note must never set. QA edge cases: 401 without a tenant Bearer; 400 for a missing tenant_id/thread_id/channel/body_ref/actor, an unknown channel, a direction other than INBOUND/OUTBOUND on a customer-facing channel, a blank body_ref, an unparseable occurred_at, or delivery_state 'NOT_APPLICABLE' on a customer-facing channel (that state is reserved for internal notes and the conv_message_dispatchable_state CHECK rejects it at the database too); 404 when thread_id names no thread. An inbound message defaults to delivery_state RECEIVED and read_state UNREAD, an outbound one to PENDING and READ, and the rollup trigger then updates the thread's channel_set, unread_count, last_message_at and status.",
        "operationId": "post_api_conversations_messages",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "message_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "thread_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "SMS",
                    "direction": "OUTBOUND",
                    "body_ref": "vault:blob/outbound-sms-1",
                    "body_preview": "Following up on your renewal",
                    "actor": "persona:{{dynamic:uuid}}",
                    "occurred_at": "2026-01-15T10:30:00Z",
                    "provider_message_key": "carrier-{{dynamic:uuid}}",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — [sdk-conversation] delivery_state 'NOT_APPLICABLE' is reserved for internal notes"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "thread_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "direction": {
                    "type": "string"
                  },
                  "body_ref": {
                    "type": "string"
                  },
                  "body_preview": {
                    "type": "string"
                  },
                  "actor": {
                    "type": "string"
                  },
                  "occurred_at": {
                    "type": "string"
                  },
                  "provider_message_key": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "thread_id": "<id from conversations>",
                "channel": "SMS",
                "direction": "OUTBOUND",
                "body_ref": "vault:blob/outbound-sms-1",
                "body_preview": "Following up on your renewal",
                "actor": "persona:{{dynamic:uuid}}",
                "occurred_at": "<generated pastdatetime>",
                "provider_message_key": "carrier-{{dynamic:uuid}}"
              }
            }
          }
        }
      }
    },
    "/api/conversations/threads/{id}": {
      "get": {
        "tags": [
          "sdk-conversation"
        ],
        "summary": "Fetch the thread and its messages",
        "description": "Returns one thread plus its messages, ordered strictly by occurred_at with received_at and message_id as tie-breakers only, so the order is total and stable across repeated reads. That ordering is the whole point: a provider tells you about a message when it gets around to it, so an SMS sent at 09:00 can reach the webhook after a reply sent at 09:04, and sorting by arrival would render the answer above the question. exclude_internal=true omits INTERNAL_NOTE rows, which is what a customer-visible transcript export wants. QA edge cases: a thread belonging to a DIFFERENT tenant returns 404, not 403 and not an empty 200 — a 403 would confirm the id exists to a caller who should not know that, and an empty 200 would do the same more quietly; an unknown id is likewise 404; requiresAuth applies to this GET (MUST-52), so no Bearer is 401; limit is clamped to 1..500 and offset floored at 0; messages is an empty array on a thread with no messages, never null.",
        "operationId": "get_api_conversations_threads_id",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "thread_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "THREAD_NOT_FOUND — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/conversations/threads": {
      "post": {
        "tags": [
          "sdk-conversation"
        ],
        "summary": "Open a thread with purpose and an eligibility snapshot",
        "description": "Opens a conversation thread for one subject, across every channel it will later carry. Collection-root create, so the handler returns 201 with { data: { thread } }. tenant_id, subject_ref and purpose are all required and none is defaulted: purpose in particular is refused rather than filled in, because a thread with no stated purpose is one nobody can decide is finished, and a placeholder would defeat the NOT NULL column that exists to prevent exactly that. channel_set starts empty and is maintained by trigger from the messages themselves rather than declared here — a caller-declared channel set drifts the first moment somebody replies by a route nobody predicted, and an inbox filtering on a stale set silently hides threads. current_eligibility_snapshot is stored verbatim with an eligibility_snapshot_at stamp; this SDK computes no eligibility of its own, so whatever the consumer sends is recorded as what it believed AT THAT TIME, which is what makes a message sent last Tuesday explainable against last Tuesday's rules. QA edge cases: the route is behind the gateway's default-deny authGate via a requireAuth preHandler, so a missing or invalid tenant Bearer is 401 (this is a GET-and-POST-alike rule — see MUST-52); a blank or whitespace-only purpose or subject_ref is a 400 from the service guard before any SQL runs; subject_kind, related_object_ref, sender_identity_ref, eligibility_snapshot and metadata are all optional and null/{} when omitted; status starts 'open' and closed_at stays NULL, enforced by the conv_thread_closed_shape constraint so a 'closed' thread with no closed_at is unrepresentable.",
        "operationId": "post_api_conversations_threads",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "thread_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "lead:{{dynamic:uuid}}",
                    "subject_kind": "lead",
                    "purpose": "win back the lapsed renewal",
                    "related_object_ref": "order:{{dynamic:uuid}}",
                    "sender_identity_ref": "mailbox:sales@tenant.example",
                    "eligibility_snapshot": {
                      "consent": true,
                      "quiet_hours": false
                    },
                    "metadata": {
                      "source": "api-regression"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — purpose is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "subject_kind": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "related_object_ref": {
                    "type": "string"
                  },
                  "sender_identity_ref": {
                    "type": "string"
                  },
                  "eligibility_snapshot": {
                    "type": "object",
                    "properties": {
                      "consent": {
                        "type": "boolean"
                      },
                      "quiet_hours": {
                        "type": "boolean"
                      }
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "source": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_ref": "lead:{{dynamic:uuid}}",
                "subject_kind": "lead",
                "purpose": "win back the lapsed renewal",
                "related_object_ref": "order:{{dynamic:uuid}}",
                "sender_identity_ref": "mailbox:sales@tenant.example",
                "eligibility_snapshot": {
                  "consent": true,
                  "quiet_hours": false
                },
                "metadata": {
                  "source": "api-regression"
                }
              }
            }
          }
        }
      }
    },
    "/api/coverage/backup-designations": {
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "A backup with a five-minute acceptance window",
        "description": "Who catches the work when the primary does not accept in time. acceptance_window_minutes is how long the primary has before it falls to the backup; zero would mean the backup is notified simultaneously, which is a different arrangement and should be said explicitly rather than arrived at through an empty field. A persona cannot back themselves up - the whole purpose of a backup is that it is somebody else, so a row saying otherwise is a silent single point of failure and the database refuses it. NOTE ON THE PAYLOAD: primary_persona_id comes from the persona producer, while backup_persona_id is a seeded id from test-config. That is deliberate on both counts - the handler REFUSES a persona backing themselves up (422), and the dependency graph is one node per METHOD+ENDPOINT so it cannot express a SECOND persona from the same producer [MUST-51]; coverage.persona_id is a loose reference with no foreign key into sdk-persona, so a seeded id is a truthful stand-in rather than a fabricated FK.",
        "operationId": "post_api_coverage_backup_designations",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "backup_designation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "primary_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "backup_persona_id": "{{var:coverage_backup_persona_id}}",
                    "scope": "primary-queue",
                    "acceptance_window_minutes": 5,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          },
          "422": {
            "description": "VALIDATION_ERROR — a persona cannot be their own backup"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "primary_persona_id": {
                    "type": "string"
                  },
                  "backup_persona_id": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  },
                  "acceptance_window_minutes": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "primary_persona_id": "<id from personas>",
                "backup_persona_id": "{{var:coverage_backup_persona_id}}",
                "scope": "primary-queue",
                "acceptance_window_minutes": 5
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every backup designation in the tenant",
        "description": "Every backup designation in the tenant, returned as data.designations. Takes no filter - the handler passes only the resolved tenant to listBackups - so a primary_persona_id query parameter is ignored rather than narrowing the result. Each row pairs a primary_persona_id with the backup_persona_id that covers it, plus the optional scope and acceptance_window_minutes. A persona designating itself is refused at write time as 422, so no row here is ever self-referential and a reader need not defend against that cycle. The tenant comes from the verified claim: a disagreeing tenant_id is 403 and no tenant at all is 400.",
        "operationId": "get_api_coverage_backup_designations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — tenant_id does not match the authenticated tenant"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/capacity-policies": {
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "Per-band limits with a reserved-headroom threshold",
        "description": "How much one persona, or everyone in a role, may hold at once. max_concurrent_by_band is keyed by the tenant OWN band names: naming the bands in the platform would be a business rule, and the first vertical with a third band would have to alter it. A band absent from the map is UNCAPPED, which is a different statement from capped at zero and both are expressible. freeze_threshold stops new assignment before the limit so headroom can be deliberately reserved, with per-band overrides. Exactly one of persona_id or role_ref: a policy naming both would leave the precedence undefined at the moment it matters.",
        "operationId": "post_api_coverage_capacity_policies",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capacity_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "max_concurrent_by_band": {
                      "urgent": 2,
                      "standard": 8
                    },
                    "freeze_threshold": 0.9,
                    "freeze_threshold_by_band": {
                      "urgent": 1
                    },
                    "daily_cap": 20,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "max_concurrent_by_band": {
                    "type": "object",
                    "properties": {
                      "urgent": {
                        "type": "integer"
                      },
                      "standard": {
                        "type": "integer"
                      }
                    }
                  },
                  "freeze_threshold": {
                    "type": "number"
                  },
                  "freeze_threshold_by_band": {
                    "type": "object",
                    "properties": {
                      "urgent": {
                        "type": "integer"
                      }
                    }
                  },
                  "daily_cap": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "max_concurrent_by_band": {
                  "urgent": 2,
                  "standard": 8
                },
                "freeze_threshold": 0.9,
                "freeze_threshold_by_band": {
                  "urgent": 1
                },
                "daily_cap": 20
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every capacity policy in the tenant",
        "description": "Every capacity policy the tenant has configured, returned as data.policies. Takes no filter - the handler passes only the resolved tenant to listCapacityPolicies - so a persona_id or role_ref query parameter is ignored rather than narrowing the result. A policy is subject-scoped by exactly one of persona_id or role_ref (the POST's CapacityPolicySubjectError enforces that), so a reader must branch on which one is populated rather than assuming persona. max_concurrent_by_band and freeze_threshold_by_band are per-band maps, and daily_cap is nullable, meaning 'no daily ceiling' rather than zero. The tenant comes from the verified claim: a disagreeing tenant_id is 403 and no tenant at all is 400.",
        "operationId": "get_api_coverage_capacity_policies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — tenant_id does not match the authenticated tenant"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/eligible": {
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "Ask who can act now, with reasons for those who cannot",
        "description": "THE core endpoint. Availability is a subtraction - schedule MINUS time-off MINUS holiday, intersected with live presence and capacity headroom - and every term stays a separate queryable fact so the answer can always say WHY somebody was skipped. Returns eligible personas sorted by most headroom first, so a router can take the head of the list, each with current load and remaining headroom. Ineligible personas are returned WITH their reasons by default, because the reasons are the point: a router that cannot explain a skip is a router nobody trusts. Reasons are COLLECTED, not short-circuited, so a persona who is both on PTO and at capacity reports both. tenant_id is taken from the credential; passing one that names a DIFFERENT tenant is a 403 rather than being silently preferred either way, so a misconfigured caller fails loudly instead of reading somebody else's roster.",
        "operationId": "get_api_coverage_eligible",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "eligible_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/gaps": {
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "Look ahead over the next week for tier 1",
        "description": "Windows in which nobody is on call for a rotation and tier. The whole value is in the tense: a gap found while an incident is escalating is not a warning, it is an outage, so this is written to be run AHEAD of the window and each gap reports how long until it opens. Defaults to tier 1, because a hole there means the first page goes nowhere, whereas a tier-2 hole with tier 1 staffed is thin cover rather than none. Abutting shifts produce NO gap - a zero-length gap at every handover would bury the real ones.",
        "operationId": "get_api_coverage_gaps",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "gap_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/holiday-calendars": {
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "A regional list with named owner",
        "description": "A holiday list scoped to a tenant AND a region, because a company operating in two countries does not share one. region is free-form so a tenant can key by country, state or site - naming the granularity here would be a business rule. maintained_by records WHO keeps it current: a holiday list nobody owns goes stale silently, and the first anyone notices is a working day that should not have been. Re-posting the same region replaces its dates.",
        "operationId": "post_api_coverage_holiday_calendars",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "holiday_calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "region": "US-TX",
                    "name": "Texas public holidays",
                    "dates": [
                      "2026-12-25",
                      "2027-01-01"
                    ],
                    "maintained_by": "people-ops",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "region": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "dates": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "maintained_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "region": "US-TX",
                "name": "Texas public holidays",
                "dates": [
                  "2026-12-25",
                  "2027-01-01"
                ],
                "maintained_by": "people-ops"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every holiday calendar in the tenant",
        "description": "Every holiday calendar the tenant maintains, returned as data.calendars. Takes no filter of any kind - the handler passes only the resolved tenant to listHolidayCalendars - so a region query parameter is accepted by the router and ignored rather than narrowing the result. Regions are per-tenant strings, not an ISO set, because a tenant's holiday regions follow its operating calendar and not a country code. The tenant comes from the verified claim: a disagreeing tenant_id in the query is 403 and no tenant at all is 400.",
        "operationId": "get_api_coverage_holiday_calendars",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — tenant_id does not match the authenticated tenant"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/on-call/current": {
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "Resolve the current audience for a rotation",
        "description": "The on-call audience at an instant, in tier order so the caller pages tier 1 before tier 2, plus whoever is manager-on-duty. The interval test is half-open, so at exactly a handover instant the outgoing shift is off and the incoming one is on - neither doubled up nor a second uncovered. Reports uncovered=true explicitly when nobody is on call: a caller that reads an empty list as \"no result\" will escalate into the void, which is precisely the failure this endpoint exists to prevent.",
        "operationId": "get_api_coverage_on_call_current",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "current_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/on-call": {
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "Tier 1 for the coming week",
        "description": "Puts a persona on call for an interval at a tier. Entries overlap on purpose - tier 1 and tier 2 are both on call at once, which is what a tier means - so there is no exclusion constraint and no \"current on-call\" column; the answer is computed at the instant it is asked about. Re-posting the same rotation, tier, persona and start extends the interval rather than duplicating it, so a roster import is safe to re-run.",
        "operationId": "post_api_coverage_on_call",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "on_call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rotation_ref": "primary",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tier": 1,
                    "starts_at": "2026-01-15T10:30:00Z",
                    "ends_at": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "rotation_ref": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "tier": {
                    "type": "integer"
                  },
                  "starts_at": {
                    "type": "string"
                  },
                  "ends_at": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "rotation_ref": "primary",
                "persona_id": "<id from personas>",
                "tier": 1,
                "starts_at": "<generated futuredatetime+1h>",
                "ends_at": "<generated futuredatetime+7d>"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every roster entry in the tenant",
        "description": "The raw on-call roster for the tenant, optionally narrowed to one rotation via the rotation_ref query parameter. Returns 200 with data.roster as an array of entries, each carrying its tier and its starts_at/ends_at window. This is deliberately NOT the same question as GET /api/coverage/on-call/current: this route lists every entry as stored, including past and future windows, while /current resolves who is on call at one instant and applies tier fallback. Reading the roster to work out who is on duty now duplicates that resolution and gets the tier precedence wrong. The tenant comes from the verified claim: a disagreeing tenant_id is 403 and no tenant at all is 400.",
        "operationId": "get_api_coverage_on_call",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — tenant_id does not match the authenticated tenant"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/coverage/presence": {
      "put": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "A manual claim of being available",
        "description": "Sets a persona live presence, recording WHERE the claim came from. A MANUAL claim outranks an automated one until manual_hold_until: somebody saying \"I am here\" must not be overwritten two seconds later by a calendar that still thinks they are in a meeting, and equally a manual toggle cannot win forever or the calendar could never recover. A CALENDAR or SYSTEM write arriving inside a live manual hold is accepted as a no-op and reports which claim currently stands, rather than failing - the sync is not wrong, it is simply outranked.",
        "operationId": "put_api_coverage_presence",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "presence_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "AVAILABLE",
                    "source": "MANUAL",
                    "manual_hold_minutes": 30,
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  },
                  "manual_hold_minutes": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "status": "AVAILABLE",
                "source": "MANUAL",
                "manual_hold_minutes": 30
              }
            }
          }
        }
      }
    },
    "/api/coverage/schedules": {
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every schedule in the tenant",
        "description": "Working schedules for the tenant, optionally narrowed to one persona. Returns the windows as written, in the persona own timezone, rather than projected into UTC - a schedule shown in a zone its owner does not work in is unreadable to the person who has to correct it.",
        "operationId": "get_api_coverage_schedules",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "schedule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "A weekday schedule in the persona own timezone",
        "description": "Upserts the recurring working windows for one persona in ONE named IANA timezone. The timezone is validated at write time rather than at the first sweep - an unresolvable zone accepted here would surface as a persona mysteriously never eligible, hours later and far from the cause. weekly_windows is an array of {weekday 0-6, start HH:MM, end HH:MM} in the persona local wall time, which is the only representation that survives a DST change without being silently wrong for an hour twice a year. One ACTIVE schedule per persona; re-posting replaces it.",
        "operationId": "post_api_coverage_schedules",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "schedule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "iana_timezone": "America/Chicago",
                    "weekly_windows": [
                      {
                        "weekday": 1,
                        "start": "09:00",
                        "end": "17:00"
                      },
                      {
                        "weekday": 2,
                        "start": "09:00",
                        "end": "17:00"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "iana_timezone": {
                    "type": "string"
                  },
                  "weekly_windows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "weekday": {
                          "type": "integer"
                        },
                        "start": {
                          "type": "string"
                        },
                        "end": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "iana_timezone": "America/Chicago",
                "weekly_windows": [
                  {
                    "weekday": 1,
                    "start": "09:00",
                    "end": "17:00"
                  },
                  {
                    "weekday": 2,
                    "start": "09:00",
                    "end": "17:00"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/coverage/time-off": {
      "post": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "A day of PTO",
        "description": "Records an interval a persona is unavailable. Intervals MAY overlap by design - a meeting inside a PTO day is not a contradiction, and eligibility takes the union - so there is no exclusion constraint. kind separates the reasons because they read differently to an operator deciding whether to interrupt somebody. source distinguishes a calendar sync from a manual entry, so a re-sync can update its own rows without overwriting what a human typed.",
        "operationId": "post_api_coverage_time_off",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "time_off_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "PTO",
                    "starts_at": "2026-01-15T10:30:00Z",
                    "ends_at": "2026-01-15T10:30:00Z",
                    "reason": "annual leave",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid credential"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "persona_id": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "starts_at": {
                    "type": "string"
                  },
                  "ends_at": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "persona_id": "<id from personas>",
                "kind": "PTO",
                "starts_at": "<generated futuredatetime+1d>",
                "ends_at": "<generated futuredatetime+2d>",
                "reason": "annual leave"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "sdk-coverage"
        ],
        "summary": "List every time-off record in the tenant",
        "description": "Time-off records for the tenant, optionally narrowed to one persona via the persona_id query parameter. Returns 200 with data.time_off as an array; an empty tenant answers 200 with [] rather than 404, because 'this persona has booked no leave' is an answer, not a missing resource. The tenant is taken from the verified claim: a tenant_id in the query that disagrees with the credential is 403, not silently preferred, and a caller with neither is 400. Unlike the POST, this route performs no date parsing, so it has no 422 path - the only refusals are the auth and tenant-resolution ones below.",
        "operationId": "get_api_coverage_time_off",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — tenant_id does not match the authenticated tenant"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/close-reasons": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Define a competitive-loss reason that demands a competitor and a learning note",
        "description": "Upserts one code in the TENANT's close-reason taxonomy, so adding a reason is an INSERT rather than a release - a hard-coded list is a claim that every business loses the same way, and it is unfalsifiable because people just pick the closest option and the report reads back the categories that shipped. The code carries its own rules: whether a closed subject may be approached again, after how many days, and whether closing on it requires naming a competitor or writing a learning note. Upsert on (tenant_id, code), hence 200 rather than 201 - re-sending a code edits it in place and is idempotent. Edge cases: code and label are required and must be non-blank after trimming (400 VALIDATION_ERROR); outcome_class defaults to 'lost' and is constrained to won/lost/disqualified/paused; reactivation_after_days together with reactivation_allowed=false is refused - 'never come back, after 90 days' is two rules that contradict each other and whichever was meant, somebody downstream reads the other; reactivation_after_days must be >= 0; sort_order defaults to 100 and only orders the picker.",
        "operationId": "post_api_crm_close_reasons",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "close_reason_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "code": "lost-to-competitor",
                    "label": "Lost to a competing option",
                    "outcome_class": "lost",
                    "reactivation_allowed": true,
                    "reactivation_after_days": 90,
                    "requires_competitor": true,
                    "requires_learning_note": true,
                    "sort_order": 10,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "outcome_class": {
                    "type": "string"
                  },
                  "reactivation_allowed": {
                    "type": "boolean"
                  },
                  "reactivation_after_days": {
                    "type": "integer"
                  },
                  "requires_competitor": {
                    "type": "boolean"
                  },
                  "requires_learning_note": {
                    "type": "boolean"
                  },
                  "sort_order": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "code": "lost-to-competitor",
                "label": "Lost to a competing option",
                "outcome_class": "lost",
                "reactivation_allowed": true,
                "reactivation_after_days": 90,
                "requires_competitor": true,
                "requires_learning_note": true,
                "sort_order": 10
              }
            }
          }
        }
      }
    },
    "/api/crm/next-actions/{id}/reschedule": {
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Push the date with a reason on the record",
        "description": "Moves an open action's due date and logs the move: from, to, reason, who pushed it and who authorised it. Returns the new due_at, the incremented push_count, the frozen original_due_at and total_slip_minutes from the FIRST commitment - which is what a count alone cannot say. 200 rather than 201 because this is an action on an existing row, not a create. Edge cases: reason is REQUIRED and a blank or whitespace-only reason is refused with 400 RESCHEDULE_REASON_REQUIRED, enforced by a table constraint as well as by the service; moving a date to the value it already has is refused with the same code - that is a no-op somebody logged, not a push; once push_count reaches the tenant's push_threshold a further push needs approved_by and is otherwise refused with 409 PUSH_THRESHOLD_REACHED, because a ceiling that only warns is not a ceiling; with no overdue_policy row for the tenant the threshold is null and pushes are unlimited; an unknown id, or one whose action is completed or cancelled, returns 404 NEXT_ACTION_NOT_FOUND; original_due_at is frozen on the first push by a trigger and never moves again; the row is locked FOR UPDATE, so concurrent pushes serialise and each gets its own seq.",
        "operationId": "post_api_crm_next_actions_id_reschedule",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reschedule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "new_due_at": "2026-01-15T10:30:00Z",
                    "reason": "Buyer moved the budget review to next week",
                    "pushed_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "approved_by": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id and new_due_at are required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NEXT_ACTION_NOT_FOUND — no open next action"
          },
          "409": {
            "description": "PUSH_THRESHOLD_REACHED — this action has already been pushed at or past the threshold - a further push needs a manager's authorisation"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "new_due_at": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "pushed_by": {
                    "type": "string"
                  },
                  "approved_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "new_due_at": "<generated futuredatetime>",
                "reason": "Buyer moved the budget review to next week",
                "pushed_by": "<id from personas>",
                "approved_by": "<id from personas>"
              }
            }
          }
        }
      }
    },
    "/api/crm/next-actions/overdue": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Read the overdue queue for the tenant",
        "description": "Lists open next actions whose due_at has passed, oldest first, each carrying minutes_overdue, push_count, original_due_at and the escalation_level it has reached. Returns the ladder actually applied in `offsets` so a caller renders the tenant's own levels rather than guessing. Edge cases: tenant_id query param is required (400 without it); escalation_level is the HIGHEST offset passed, not the first, so an action a week late is not reported at 'nudge' and buried under actions an hour late; escalation_level is null when the action is overdue but has not yet passed the first configured offset; with no overdue_policy row for the tenant, offsets is [] and every escalation_level is null - the queue still lists everything overdue, it just has no levels to name; subject_kind selects the kind-specific policy and falls back to the tenant-wide one; limit is clamped to 1..1000 (default 100), so an absurd limit degrades instead of erroring; an empty entries[] is a normal answer, not a 404.",
        "operationId": "get_api_crm_next_actions_overdue",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "overdue_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/pipeline/aging": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Read the aging report for the tenant",
        "description": "Reports how long each subject has sat in its current stage and how long it has been silent, counted in BUSINESS days: a deal that goes quiet on Friday is not two days stale on Sunday, and a queue that says otherwise trains people to ignore it every Monday. Edge cases: tenant_id query param is required (400 without it); business time comes from sdk-sla's calendars through a registered hook, and when nothing is wired the response says so with business_days_available=false and leaves business_days_in_stage null rather than silently answering in calendar days - calendar_days_in_stage is always present and is clearly labelled as the calendar figure; last_activity_at is set only by an explicit activity record, never by an edit, because aging is about silence and fixing a phone number is not contact; min_business_days filters on the business figure when one exists and falls back to the calendar figure when it does not; only the OPEN stage entry per subject is reported (exited_at IS NULL); limit is clamped to 1..2000 (default 200); an empty entries[] is a normal answer for a tenant with no stage entries, not a 404.",
        "operationId": "get_api_crm_pipeline_aging",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "aging_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/crm/subjects/{subject_ref}/next-action": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Read the action just committed",
        "description": "Returns the single open NEXT action for a subject, with its five committed elements plus push_count and original_due_at so a caller can see how far the commitment has already slipped. Edge cases: tenant_id query param is required (400 without it); a subject that has never had an action, or whose only action was completed, cancelled or superseded, returns 404 NotFound rather than an empty object - 'no commitment' is a state a caller must handle, not an empty shape it can render; the lookup is scoped to the tenant, so another tenant's action on the same subject_ref is invisible.",
        "operationId": "get_api_crm_subjects_subject_ref_next_action",
        "parameters": [
          {
            "name": "subject_ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "subject_ref path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "next_action_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — no open next action for this subject"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Commit a call with all five elements of the commitment",
        "description": "Commits the single open NEXT action for ANY subject - a lead, a contact, a ticket or a deal - addressed by its subject_ref (`<kind>:<id>`, e.g. `lead:abc123`). All five elements of the commitment are required: action_type, owner_persona_id, due_at (an exact instant), purpose and intended_outcome. Edge cases: a missing or blank element returns 400 NEXT_ACTION_INCOMPLETE with EVERY missing element listed individually in details[] (field + message), never just the first one, so a client can render each against its own input; committing again SUPERSEDES the prior open action (it is cancelled, not queued behind), so exactly one action is open per subject at any time; subject_kind is parsed from the ref prefix and cannot be sent separately, so a ref and a kind can never disagree; the legacy deal FK is back-filled only when the ref is `deal:<uuid>` AND that deal exists for the tenant, so naming a deal that is gone still records the action instead of failing on a constraint; due_at is stored as sent, and a past instant is accepted (it simply lands in the overdue queue immediately).",
        "operationId": "post_api_crm_subjects_subject_ref_next_action",
        "parameters": [
          {
            "name": "subject_ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "subject_ref path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "next_action_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "action_type": "call",
                    "owner_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "due_at": "2026-01-15T10:30:00Z",
                    "purpose": "Confirm the budget holder and the decision date",
                    "intended_outcome": "A named budget holder and a decision date agreed in writing",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "action_type": {
                    "type": "string"
                  },
                  "owner_persona_id": {
                    "type": "string"
                  },
                  "due_at": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "intended_outcome": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "action_type": "call",
                "owner_persona_id": "<id from personas>",
                "due_at": "<generated futuredatetime>",
                "purpose": "Confirm the budget holder and the decision date",
                "intended_outcome": "A named budget holder and a decision date agreed in writing"
              }
            }
          }
        }
      }
    },
    "/api/crm/subjects/{subject_ref}/save-gate": {
      "get": {
        "tags": [
          "sdk-crm"
        ],
        "summary": "Allowed once a complete action is committed",
        "description": "Answers whether a subject may be saved, and if not, exactly what is missing. Returns {allowed, subject_ref, missing[], next_action_id}, where missing[] carries one {field, message} entry per element - never collapsed into a single sentence, because a verdict of 'next action incomplete' forces the user to guess which of five fields is wrong. Edge cases: tenant_id query param is required (400 without it); the verdict is 200 whether allowed is true or false - a refusal is an answer, not an error, so a client can render it inline and there is no 404 for an unknown subject either; a subject with NO open action is refused with the single element `next_action`; a subject whose open action is missing several elements gets one entry per element in one pass; the gate is subject-kind agnostic and answers identically for a lead, a ticket or a deal.",
        "operationId": "get_api_crm_subjects_subject_ref_save_gate",
        "parameters": [
          {
            "name": "subject_ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "subject_ref path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "save_gate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/capabilities/estimate": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Quote a capability for this tenant",
        "description": "Quotes one capability for this tenant before anything is held: the credit price, the credits currently available (balance minus the part already reserved for in-flight requests) and an explicit affordable flag. The flag is stated rather than left to the caller to compute, because a caller that gets the comparison wrong finds out at the reserve, halfway through a flow. QA edge cases: an unknown or inactive capability_key is 404 CAPABILITY_NOT_FOUND, not an empty quote; a tenant with no credit account at all quotes available=0 and affordable=false rather than erroring, because \"you have no account\" and \"you have no credits\" lead to the same next step for the caller; a tenant-specific negotiated price is preferred over the platform price; available NEVER includes credits already held by an unsettled request. NOTE ON capability_key: it resolves from {{var:}} and not from a producer because NO api creates a capability — the catalog is platform reference data seeded by tests/setup_scripts/data_credits_catalog.sql, and its provider bindings are deliberately unreachable from any tenant-scoped route. MUST-46 permits {{var:}} precisely for a value nothing produces.",
        "operationId": "get_api_capabilities_estimate",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "estimate_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — capability_key is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "CAPABILITY_NOT_FOUND — no capability 'x' is available to this tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/capabilities": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "List the outcomes this tenant can buy",
        "description": "The catalog in the tenant's own language: an outcome-named key (\"validate.phone\"), a label, a description and a price in credits. What it deliberately does NOT contain is any trace of who serves the outcome - no provider name, no credentials reference, no routing detail and no true vendor cost - because a tenant that learns which vendor answered starts building on that vendor, and the day it is replaced their integration breaks along with the abstraction. The response is built by naming the fields a tenant may see rather than by removing fields from a row, so a column added to the catalog later cannot appear here by default. A tenant with a negotiated price sees THEIR price: a tenant-scoped catalog row overrides the platform default for the same key, and exactly one row per key is returned. QA edge cases: a tenant with no negotiated prices sees the platform catalog; inactive capabilities are omitted entirely rather than returned with a flag; the list is never empty in a seeded environment because the catalog is reference data, so an empty array means the seed did not run.",
        "operationId": "get_api_capabilities",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/capability-requests": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "List recent requests",
        "description": "Lists this tenant's requests newest first, each with the outcome-named capability key, its status, the settlement outcome once it has one, whether it was served from cache, and the credits reserved and charged. It carries no provider identity for the same reason as every other tenant-scoped read here. QA edge cases: credits_charged is 0 for an unsettled request AND for a settled one that cost nothing (a no-match, a provider failure or a cache hit) - the outcome field is what distinguishes them, not the number; filtering by status accepts only the real request_status values and an unknown one returns an empty list rather than an error; limit is clamped to 500.",
        "operationId": "get_api_capability_requests",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "capability_request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Ask for an outcome about a subject",
        "description": "Creates the request and HOLDS the quoted credits in one transaction. The hold exists because both alternatives are worse: charging up front turns every no-match into a refund somebody has to promise to make later, and charging afterwards lets a thousand concurrent requests run against a balance of five. The subject is a FINGERPRINT, never the raw phone number or email - the broker does not need the raw value to bill, cache or audit, and a table of everything every tenant ever looked up is a breach waiting for an excuse. The role_ref decides the governance: a REQUEST_ONLY role (or any request at or above the bulk-approval threshold, whatever the role) comes back with status PENDING_APPROVAL and cannot execute until it is approved; a FULL role comes back APPROVED. QA edge cases: a refused hold writes NOTHING - no orphan request is left behind, because the whole reserve is one transaction; two concurrent reserves cannot both spend the last credit (the account row is read FOR UPDATE); the same subject may be requested repeatedly and each request gets its own hold; a daily-capped role that has exhausted its window is 403 DAILY_CAP_EXCEEDED, which is NOT the same as 402 INSUFFICIENT_CREDITS - the tenant HAS the credits, this requester may not spend them.",
        "operationId": "post_api_capability_requests",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capability_request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capability_key": "{{var:data_credits_capability_key}}",
                    "subject_fingerprint": "fp-{{dynamic:slug}}",
                    "requested_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role_ref": "analyst",
                    "metadata": {
                      "source": "api-test"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — capability_key is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS — this request needs N credits and M are available"
          },
          "403": {
            "description": "DAILY_CAP_EXCEEDED — this role has spent X of Y credits in the last 24 hours"
          },
          "404": {
            "description": "CAPABILITY_NOT_FOUND — no such capability for this tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "capability_key": {
                    "type": "string"
                  },
                  "subject_fingerprint": {
                    "type": "string"
                  },
                  "requested_by_persona_id": {
                    "type": "string"
                  },
                  "role_ref": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "source": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "capability_key": "{{var:data_credits_capability_key}}",
                "subject_fingerprint": "fp-{{dynamic:slug}}",
                "requested_by_persona_id": "<id from personas>",
                "role_ref": "{{static:analyst}}",
                "metadata": {
                  "source": "api-test"
                }
              }
            }
          }
        }
      }
    },
    "/api/capability-requests/{request_id}/approve": {
      "post": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Approve a waiting request",
        "description": "Records the decision a PENDING_APPROVAL request is waiting for. Approving lets it execute and stamps the sdk-approval reference; approving twice is a NO-OP that returns the original decision time, because an approval webhook that retries is ordinary and failing the retry would leave a decision that WAS made looking like one that was not. Sending approved=false REFUSES the request and gives the held credits back - the endpoint has to be able to do this, because a refusal that only changed a status would leave the hold sitting against the tenant's available balance forever, which is the quiet version of losing their money. A refusal is recorded as a CANCELLATION of the reservation rather than as a settlement: the four settlement outcomes are all statements about a lookup that happened, and a refused request never looked at anything. QA edge cases: a refusal with no reason is 400 - \"it was cancelled\" with no reason is unanswerable three weeks later; approving a request that is already APPROVED is a no-op, but REJECTING one is 409 NOT_AWAITING_APPROVAL because the hold is already committed to it; rejecting an already-rejected request releases nothing a second time.",
        "operationId": "post_api_capability_requests_request_id_approve",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "approve_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "approved": true,
                    "approval_ref": "apr-{{dynamic:slug}}",
                    "decided_by": "qa-manager",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — a refusal must carry a reason"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "CAPABILITY_REQUEST_NOT_FOUND — no capability request <id>"
          },
          "409": {
            "description": "NOT_AWAITING_APPROVAL — request <id> is <status>, not waiting for an approval decision"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "approved": {
                    "type": "boolean"
                  },
                  "approval_ref": {
                    "type": "string"
                  },
                  "decided_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "approved": true,
                "approval_ref": "apr-{{dynamic:slug}}",
                "decided_by": "{{static:qa-manager}}"
              }
            }
          }
        }
      }
    },
    "/api/capability-requests/{request_id}/execute": {
      "post": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Execute an approved request",
        "description": "Runs the request and settles the hold in one call. The result cache is consulted BEFORE any provider, so a repeat question about the same subject inside its TTL is answered free and calls no vendor at all - a cache consulted afterwards would save the credit and still spend the call. Otherwise the provider chain is walked by priority and live health: a provider that fails is stepped over invisibly, a no-match does NOT stop the walk (the next vendor may hold the record the first one lacks), and only a match stops it. The settlement is the point: MATCHED charges exactly the quoted credits, and NO_MATCH, TECHNICAL_FAILURE and CACHE_HIT all settle to ZERO and release the hold, because a tenant pays for answers, not for attempts. The `subject` field is the raw value the provider needs; it is passed through and NOT stored - the request keeps only the fingerprint. QA edge cases: executing a PENDING_APPROVAL request is 409 APPROVAL_REQUIRED and invokes no provider; \"everybody looked and nobody has it\" (NO_MATCH) is kept distinct from \"nobody managed to look\" (TECHNICAL_FAILURE), and an unwired provider adapter produces the latter, never the former; executing a second time re-settles identically and charges once, but a second execution that would settle DIFFERENTLY is 409 SETTLEMENT_CONFLICT rather than being merged.",
        "operationId": "post_api_capability_requests_request_id_execute",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "execute_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject": "+15551234567",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "CAPABILITY_REQUEST_NOT_FOUND — no capability request <id>"
          },
          "409": {
            "description": "APPROVAL_REQUIRED — request <id> needs an approval decision before it can execute"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject": "+15551234567"
              }
            }
          }
        }
      }
    },
    "/api/capability-requests/{request_id}": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Read one request",
        "description": "One request: the capability as the tenant sees it, the settlement outcome, the result, the credits reserved and the credits actually charged. The fallback that produced the result - which provider was tried first, which one answered, how long each took and what they truly cost us - is recorded internally and appears in NONE of this. QA edge cases: a request id belonging to another tenant is 404 rather than 403, because confirming that an id exists elsewhere is itself a leak; result is null until the request has executed; credits_charged is 0 for every settlement except MATCHED.",
        "operationId": "get_api_capability_requests_request_id",
        "parameters": [
          {
            "name": "request_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "request_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "capability_request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "CAPABILITY_REQUEST_NOT_FOUND — no capability request <id>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/credits/balance": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Read the credit balance",
        "description": "Three numbers: the balance, the part of it reserved for in-flight requests, and what is actually available (balance minus reserved). Available is a subtraction and is deliberately NOT stored anywhere - a third column would be a second source of truth for a number that is already implied, and the day the two disagree nobody can say which is right. QA edge cases: a tenant with no credit account is 404 CREDIT_ACCOUNT_NOT_FOUND rather than a zero balance, because \"no account\" and \"no credits\" are different situations with different fixes; reserved never exceeds balance (the database refuses it), so available is never negative; a hold released by a zero settlement restores available immediately, without touching balance.",
        "operationId": "get_api_credits_balance",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "balance_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "CREDIT_ACCOUNT_NOT_FOUND — this tenant has no credit account"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/credits/budgets": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "List the budget policies",
        "description": "Lists every role policy this tenant has set, in role order. QA edge cases: a role that has NO policy does not appear here, and its requests still require approval - the absence IS the policy, so an empty list does not mean \"everybody may spend freely\"; daily_cap and bulk_approval_threshold are null when the mode does not use them, rather than zero, because a cap of zero would mean nobody may spend anything and that is a different statement.",
        "operationId": "get_api_credits_budgets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "budget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Cap a role at a rolling daily limit with a bulk gate",
        "description": "Sets one role's budget policy, replacing whatever that role had. REQUEST_ONLY means every request waits for an approval decision; DAILY_CAP allows spending up to a limit over a ROLLING 24 hours (rolling, not calendar - a calendar day resets at a moment somebody has to pick a timezone for, and a tenant in the wrong one gets two days of spend inside one working day); FULL spends freely. bulk_approval_threshold outranks all three: a single request at or above it needs approval regardless of role, because one enormous request is a different decision from the thousand small ones the role was trusted with. Spend against the cap is derived from the append-only ledger rather than from a counter, so it can always be re-derived and never quietly drifts; holds do not count, only charges - otherwise an in-flight request would eat into a cap that a no-match is about to hand back. QA edge cases: DAILY_CAP with no daily_cap is 422 - a policy that reads as a limit and enforces nothing is worse than no policy at all, because the dashboard says \"capped\" and the spend says otherwise; a negative cap or threshold is 422; the same role sent twice is one policy, not two (the upsert is keyed on tenant + role); a role with NO policy at all is not refused and not waved through - its requests come back PENDING_APPROVAL, because absent policy could mean \"not configured yet\" or \"no restriction\" and the two are indistinguishable from here.",
        "operationId": "put_api_credits_budgets",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "budget_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role_ref": "analyst-{{dynamic:slug}}",
                    "mode": "DAILY_CAP",
                    "daily_cap": 250,
                    "bulk_approval_threshold": 50,
                    "is_active": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — mode must be one of REQUEST_ONLY, DAILY_CAP, FULL"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "422": {
            "description": "VALIDATION_ERROR — a DAILY_CAP policy must carry a daily_cap"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "role_ref": {
                    "type": "string"
                  },
                  "mode": {
                    "type": "string"
                  },
                  "daily_cap": {
                    "type": "integer"
                  },
                  "bulk_approval_threshold": {
                    "type": "integer"
                  },
                  "is_active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "role_ref": "analyst-{{dynamic:slug}}",
                "mode": "DAILY_CAP",
                "daily_cap": 250,
                "bulk_approval_threshold": 50,
                "is_active": true
              }
            }
          }
        }
      }
    },
    "/api/credits/ledger": {
      "get": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Export the ledger for one request",
        "description": "Every credit movement in order, with the balance and reserved deltas kept SEPARATE and the account as it stood after each entry. Separate deltas are what let the export show \"quoted 5, charged 0, released 5\" instead of a single net number that hides the quote - which is exactly the question a disputed invoice asks. Entry types: GRANT (credits added), RESERVATION (a hold, moving reserved only), CHARGE (a match, moving both), RELEASE (a hold handed back, naming why nothing was charged), REFUND and ADJUSTMENT. The table is append-only in the database: corrections are new ADJUSTMENT entries, never edits, so an export can be trusted to be what happened. QA edge cases: entries survive the request they describe being purged, because a financial record must outlive the operational row (request_id stays as a fact, not as a live join); filtering by request_id gives the whole story of one request; entry_no is a total order, so two entries in the same millisecond still export in the order they happened; the true vendor cost appears nowhere in this export.",
        "operationId": "get_api_credits_ledger",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "ledger_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/credits/reservations": {
      "post": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Hold credits for a self-run lookup",
        "description": "The MANUAL lane. Holds the quoted credits for a capability that the caller will execute and settle itself, and returns the balance as it stands after the hold. It is deliberately distinct from POST /api/capability-requests + /execute, which is the BROKERED lane where the broker picks the provider, walks the fallback chain and settles for you; here the caller performs the lookup. Both lanes hold credits the same way and settle under the same rules - what differs is who does the looking. QA edge cases: the same governance applies, so a REQUEST_ONLY role gets a PENDING_APPROVAL reservation it cannot settle until it is approved; a hold that cannot be covered is 402 and writes nothing; the returned balance already reflects the hold, so available has gone down while balance has not moved.",
        "operationId": "post_api_credits_reservations",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reservation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capability_key": "{{var:data_credits_capability_key}}",
                    "subject_fingerprint": "fp-{{dynamic:slug}}",
                    "role_ref": "analyst",
                    "requested_by_persona_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "metadata": {
                      "lane": "manual"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — capability_key is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "402": {
            "description": "INSUFFICIENT_CREDITS — this request needs N credits and M are available"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "capability_key": {
                    "type": "string"
                  },
                  "subject_fingerprint": {
                    "type": "string"
                  },
                  "role_ref": {
                    "type": "string"
                  },
                  "requested_by_persona_id": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "lane": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "capability_key": "{{var:data_credits_capability_key}}",
                "subject_fingerprint": "fp-{{dynamic:slug}}",
                "role_ref": "{{static:analyst}}",
                "requested_by_persona_id": "<id from personas>",
                "metadata": {
                  "lane": "manual"
                }
              }
            }
          }
        }
      }
    },
    "/api/credits/reservations/{reservation_id}/settle": {
      "post": {
        "tags": [
          "sdk-data-credits"
        ],
        "summary": "Settle a hold as a no-match, charging nothing",
        "description": "Closes a hold with one of the four settlement outcomes. MATCHED charges exactly the quoted credits; NO_MATCH, TECHNICAL_FAILURE and CACHE_HIT settle to ZERO and hand the hold straight back, because a tenant pays for answers and not for attempts. The rule is enforced by the database as well as by this handler - these are promises about somebody's money, and a promise kept only by the current version of one function is not kept. Settling is IDEMPOTENT on an identical retry: the same outcome and the same credits return the same result, charge once, and keep the FIRST settlement time, which is what an at-least-once caller needs. QA edge cases: a retry asserting a DIFFERENT outcome is 409 SETTLEMENT_CONFLICT rather than being merged, because merging means the last retry to arrive decides what the tenant paid; an outcome outside the four values is 400 with the valid set named; a reservation belonging to another tenant is 404; the ledger gains a CHARGE entry for a match and a RELEASE entry naming WHY nothing was charged for the other three, so an export can tell them apart.",
        "operationId": "post_api_credits_reservations_reservation_id_settle",
        "parameters": [
          {
            "name": "reservation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "reservation_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "settle_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "outcome": "NO_MATCH",
                    "result": null,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — outcome must be one of MATCHED, NO_MATCH, TECHNICAL_FAILURE, CACHE_HIT"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "RESERVATION_NOT_FOUND — no reservation <id>"
          },
          "409": {
            "description": "SETTLEMENT_CONFLICT — request <id> already settled as X for N credits"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "outcome": {
                    "type": "string"
                  },
                  "result": {
                    "nullable": true
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "outcome": "NO_MATCH",
                "result": null
              }
            }
          }
        }
      }
    },
    "/api/imports/mapping-templates": {
      "get": {
        "tags": [
          "sdk-import"
        ],
        "summary": "List the tenant's mapping templates",
        "description": "List a tenant's mapping templates, grouped by slug with the newest version first, optionally narrowed by slug, kind or active flag. limit is clamped to 1..500 (default 50). Required: tenant_id query param.",
        "operationId": "get_api_imports_mapping_templates",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "mapping_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Create a custom mapping template",
        "description": "Create a reusable, versioned column mapping. Version 1 is created here; later revisions are NEW ROWS via POST /:template_id/version, never edits — a template referenced by a committed run is frozen by database trigger, because rewriting the mapping that produced already-landed rows would make that run's lineage a lie. crosswalk_strategy decides what happens when an incoming external id already maps to a known entity. kind is certified (platform-curated) or custom (the tenant's own). UNIQUE(tenant_id, slug, version). Required: tenant_id, slug, name.",
        "operationId": "post_api_imports_mapping_templates",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "mapping_template_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "contacts-{{dynamic:slug}}",
                    "name": "Contact extract mapping",
                    "description": "Maps a partner contact extract onto canonical targets",
                    "kind": "custom",
                    "field_map": {},
                    "transforms": [],
                    "value_crosswalks": {},
                    "crosswalk_strategy": "preserve_existing",
                    "created_by": "qa-runner",
                    "metadata": {
                      "origin": "qa"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, slug and name are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "DUPLICATE_TEMPLATE_VERSION — a template with this slug and version already exists for the tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "field_map": {
                    "type": "object",
                    "properties": {}
                  },
                  "transforms": {
                    "type": "array",
                    "items": {}
                  },
                  "value_crosswalks": {
                    "type": "object",
                    "properties": {}
                  },
                  "crosswalk_strategy": {
                    "type": "string"
                  },
                  "created_by": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "origin": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "slug": "contacts-{{dynamic:slug}}",
                "name": "Contact extract mapping",
                "description": "Maps a partner contact extract onto canonical targets",
                "kind": "custom",
                "field_map": {},
                "transforms": [],
                "value_crosswalks": {},
                "crosswalk_strategy": "preserve_existing",
                "created_by": "qa-runner",
                "metadata": {
                  "origin": "qa"
                }
              }
            }
          }
        }
      }
    },
    "/api/imports/mapping-templates/{template_id}/version": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Publish version 2 of the template",
        "description": "Publish the next version of a template as a NEW ROW, carrying the parent version's values for anything not overridden and recording parent_template_id so \"which mapping did v3 come from\" stays answerable. This is the supported way to change a template: once any run that referenced it has committed, the database trigger freezes its definition columns, since rewriting them would make that run's lineage describe a mapping that no longer exists. Returns 201 — a version is a new resource, not an edit. Required: tenant_id.",
        "operationId": "post_api_imports_mapping_templates_template_id_version",
        "parameters": [
          {
            "name": "template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "template_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "Contact extract mapping v2",
                    "description": "Adds the postal code column",
                    "field_map": {},
                    "transforms": [],
                    "value_crosswalks": {},
                    "crosswalk_strategy": "add_alias",
                    "created_by": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "MAPPING_TEMPLATE_NOT_FOUND — mapping template <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "field_map": {
                    "type": "object",
                    "properties": {}
                  },
                  "transforms": {
                    "type": "array",
                    "items": {}
                  },
                  "value_crosswalks": {
                    "type": "object",
                    "properties": {}
                  },
                  "crosswalk_strategy": {
                    "type": "string"
                  },
                  "created_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "name": "Contact extract mapping v2",
                "description": "Adds the postal code column",
                "field_map": {},
                "transforms": [],
                "value_crosswalks": {},
                "crosswalk_strategy": "add_alias",
                "created_by": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/imports/runs": {
      "get": {
        "tags": [
          "sdk-import"
        ],
        "summary": "List runs for the tenant",
        "description": "List a tenant's import runs, newest first, optionally narrowed by status or source kind. limit is clamped to 1..500 (default 50). Returns an empty array rather than a 404 when nothing matches. Required: tenant_id query param.",
        "operationId": "get_api_imports_runs",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Open a run for an uploaded delimited file",
        "description": "Open a governed import. The run is the unit of governance: preview, mapping, transform plan, dry run, commit and rollback all hang off it. UNIQUE(tenant_id, file_fingerprint, source_kind) is the COMMIT IDEMPOTENCY KEY, so submitting the same file for the same source twice is refused with 409 DUPLICATE_IMPORT_RUN carrying existing_run_id — the caller almost always wants to continue that run rather than fork a second one. rollback_window_hours sets how long after commit an undo stays possible (default 24h); the DEADLINE itself is derived at commit time and cannot be supplied or extended. Required: tenant_id, source_kind, file_fingerprint.",
        "operationId": "post_api_imports_runs",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "source_kind": "csv_upload",
                    "file_fingerprint": "fp-{{dynamic:uuid}}",
                    "source_ref": "upload://{{dynamic:uuid}}",
                    "file_name": "contacts-{{dynamic:slug}}.csv",
                    "attestation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "row_count": 2,
                    "rollback_window_hours": 24,
                    "started_by": "qa-runner",
                    "metadata": {
                      "channel": "qa"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, source_kind and file_fingerprint are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "DUPLICATE_IMPORT_RUN — file <fingerprint> was already submitted for this source"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "source_kind": {
                    "type": "string"
                  },
                  "file_fingerprint": {
                    "type": "string"
                  },
                  "source_ref": {
                    "type": "string"
                  },
                  "file_name": {
                    "type": "string"
                  },
                  "attestation_id": {
                    "type": "string"
                  },
                  "row_count": {
                    "type": "integer"
                  },
                  "rollback_window_hours": {
                    "type": "integer"
                  },
                  "started_by": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "channel": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "source_kind": "csv_upload",
                "file_fingerprint": "fp-{{dynamic:uuid}}",
                "source_ref": "upload://{{dynamic:uuid}}",
                "file_name": "contacts-{{dynamic:slug}}.csv",
                "attestation_id": "<generated uuid>",
                "row_count": 2,
                "rollback_window_hours": 24,
                "started_by": "qa-runner",
                "metadata": {
                  "channel": "qa"
                }
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/commit": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Commit the dry-run-verified rows",
        "description": "Land every row and mark the run complete. IDEMPOTENT BY CONSTRUCTION: entity keys are derived from run_id + the row's own content fingerprint, lineage is written under UNIQUE(run_id, entity_kind, entity_id, action) with ON CONFLICT DO NOTHING, and a transaction-scoped advisory lock stops two workers committing the same run — so interrupting a commit and retrying it produces an identical entity set, and a commit of an already-complete run returns replayed:true without changing anything. Refuses without a signed source-rights attestation (422). A place is written as its own entity plus a located_at relationship, never as columns on the person. CONSENT IS NEVER FABRICATED: declare the consent columns and a receipt is created ONLY for a recognised affirmative value that also carries a capture date that parses; a blank, a generic placeholder (\"n/a\", \"unknown\", \"-\"), an unrecognised value or a missing date all land the row WITHOUT a receipt and file an explained exception. Returns 200: it moves an existing run. Required: tenant_id, rows[].",
        "operationId": "post_api_imports_runs_run_id_commit",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "commit_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rows": [
                      {
                        "full_name": "Ada Lovelace",
                        "email": "qa.user@example.com",
                        "street_address": "1 Analytical Way",
                        "city": "London",
                        "country": "GB",
                        "external_id": "CRM-{{dynamic:uuid}}",
                        "contact_ok": "yes",
                        "consent_date": "2026-01-05T10:00:00Z"
                      },
                      {
                        "full_name": "Alan Turing",
                        "email": "qa.user@example.com",
                        "street_address": "2 Bombe Road",
                        "city": "Manchester",
                        "country": "GB",
                        "external_id": "CRM-{{dynamic:uuid}}",
                        "contact_ok": "",
                        "consent_date": ""
                      }
                    ],
                    "consent": {
                      "value_column": "contact_ok",
                      "purpose": "outreach",
                      "captured_at_column": "consent_date"
                    },
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and rows[] are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "IMPORT_RUN_LOCKED — run <id> is already being committed by another worker"
          },
          "422": {
            "description": "ATTESTATION_NOT_SIGNED — run <id> has no signed source-rights attestation — the commit is refused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "full_name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "street_address": {
                          "type": "string"
                        },
                        "city": {
                          "type": "string"
                        },
                        "country": {
                          "type": "string"
                        },
                        "external_id": {
                          "type": "string"
                        },
                        "contact_ok": {
                          "type": "string"
                        },
                        "consent_date": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "consent": {
                    "type": "object",
                    "properties": {
                      "value_column": {
                        "type": "string"
                      },
                      "purpose": {
                        "type": "string"
                      },
                      "captured_at_column": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "rows": [
                  {
                    "full_name": "Ada Lovelace",
                    "email": "<generated email>",
                    "street_address": "1 Analytical Way",
                    "city": "London",
                    "country": "GB",
                    "external_id": "CRM-{{dynamic:uuid}}",
                    "contact_ok": "yes",
                    "consent_date": "2026-01-05T10:00:00Z"
                  },
                  {
                    "full_name": "Alan Turing",
                    "email": "<generated email>",
                    "street_address": "2 Bombe Road",
                    "city": "Manchester",
                    "country": "GB",
                    "external_id": "CRM-{{dynamic:uuid}}",
                    "contact_ok": "",
                    "consent_date": ""
                  }
                ],
                "consent": {
                  "value_column": "contact_ok",
                  "purpose": "outreach",
                  "captured_at_column": "consent_date"
                },
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/dry-run": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Dry-run the mapped rows",
        "description": "Show what the commit WOULD do — new, exact-link, review-case, related-entity and invalid counts, plus the governance verdicts — without writing anything. The guarantee is enforced, then proven, then rolled back: the simulation runs in a READ ONLY transaction (a write raises 25006 at the source), then checks pg_current_xact_id_if_assigned() as belt and braces, then rolls back unconditionally. It reuses the commit's own transform code path, so the counts describe what will actually happen rather than a parallel estimate that drifts. Governance verdicts report an unsigned attestation, an unconfirmed mapping, the columns that will be tokenized, and the review backlog. Recording that the dry run happened is a separate, deliberate write outside the proven region. Requires the transform plan (409 TRANSFORM_PLAN_REQUIRED otherwise). Required: tenant_id, rows[].",
        "operationId": "post_api_imports_runs_run_id_dry_run",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "dry_run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rows": [
                      {
                        "full_name": "Ada Lovelace",
                        "email": "qa.user@example.com",
                        "street_address": "1 Analytical Way",
                        "city": "London",
                        "country": "GB",
                        "external_id": "CRM-{{dynamic:uuid}}",
                        "contact_ok": "yes",
                        "consent_date": "2026-01-05T10:00:00Z"
                      },
                      {
                        "full_name": "Alan Turing",
                        "email": "qa.user@example.com",
                        "street_address": "2 Bombe Road",
                        "city": "Manchester",
                        "country": "GB",
                        "external_id": "CRM-{{dynamic:uuid}}",
                        "contact_ok": "",
                        "consent_date": ""
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and rows[] are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "TRANSFORM_PLAN_REQUIRED — build the transform plan before running a dry run"
          },
          "500": {
            "description": "DRY_RUN_WROTE — the dry run acquired transaction id <xid> — it wrote to the database"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "full_name": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "street_address": {
                          "type": "string"
                        },
                        "city": {
                          "type": "string"
                        },
                        "country": {
                          "type": "string"
                        },
                        "external_id": {
                          "type": "string"
                        },
                        "contact_ok": {
                          "type": "string"
                        },
                        "consent_date": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "rows": [
                  {
                    "full_name": "Ada Lovelace",
                    "email": "<generated email>",
                    "street_address": "1 Analytical Way",
                    "city": "London",
                    "country": "GB",
                    "external_id": "CRM-{{dynamic:uuid}}",
                    "contact_ok": "yes",
                    "consent_date": "2026-01-05T10:00:00Z"
                  },
                  {
                    "full_name": "Alan Turing",
                    "email": "<generated email>",
                    "street_address": "2 Bombe Road",
                    "city": "Manchester",
                    "country": "GB",
                    "external_id": "CRM-{{dynamic:uuid}}",
                    "contact_ok": "",
                    "consent_date": ""
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/exceptions": {
      "get": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Download the exception file for the committed run",
        "description": "Every row that did not land, with the ORIGINAL input kept verbatim so the operator fixes and re-submits their own data rather than the platform's interpretation of it. reason_code says why: INVALID_VALUE (the value could not be transformed), NEEDS_REVIEW (a human decision is required first), CONSENT_NOT_EVIDENCED (an affirmative marker with no capture date, a blank, or a generic placeholder — none of which is consent). Ordered by row number so it lines up with the source file. limit is clamped to 1..5000 (default 500). Required: tenant_id query param.",
        "operationId": "get_api_imports_runs_run_id_exceptions",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "exception_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/imports/runs/{run_id}": {
      "get": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Read the run created by the producer",
        "description": "Read one run together with its lineage: every entity the run created, the action that created it, and whether it has been reversed. The lineage is returned inline because it is what makes a rollback possible at all — a created entity with no lineage row is indistinguishable from one a human made. Required: tenant_id query param.",
        "operationId": "get_api_imports_runs_run_id",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "run_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/imports/runs/{run_id}/mapping": {
      "put": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Confirm every proposed column mapping",
        "description": "Apply a human's explicit per-field decisions and move the run to mapping. ONLY the columns named in confirmations[] become confirmed; everything else stays inert, so a partially reviewed mapping commits exactly the part that was reviewed. A confirmation that differs from the suggestion is recorded as a human override with the assistant's original proposal kept in the reason, which is what makes the mapping auditable later. A confirmation for an unknown column or a target outside the canonical vocabulary is refused with 422. Returns 200 (upsert of the run's mapping). Required: tenant_id, confirmations[].",
        "operationId": "put_api_imports_runs_run_id_mapping",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "mapping_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "confirmations": [
                      {
                        "source_column": "full_name",
                        "target": "person.full_name",
                        "confirmed_by": "qa-runner"
                      },
                      {
                        "source_column": "email",
                        "target": "contact.email",
                        "confirmed_by": "qa-runner"
                      },
                      {
                        "source_column": "street_address",
                        "target": "place.address_line1",
                        "confirmed_by": "qa-runner"
                      },
                      {
                        "source_column": "city",
                        "target": "place.locality",
                        "confirmed_by": "qa-runner"
                      },
                      {
                        "source_column": "country",
                        "target": "place.country",
                        "confirmed_by": "qa-runner"
                      },
                      {
                        "source_column": "external_id",
                        "target": "external.id",
                        "confirmed_by": "qa-runner",
                        "external_system": "partner-extract"
                      }
                    ],
                    "mapping_template_id": null,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and confirmations[] are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "PREVIEW_REQUIRED — run the preview before confirming a mapping"
          },
          "422": {
            "description": "UNKNOWN_MAPPING_COLUMN — no column named '<name>' in this run's preview"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "confirmations": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "source_column": {
                          "type": "string"
                        },
                        "target": {
                          "type": "string"
                        },
                        "confirmed_by": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "mapping_template_id": {
                    "nullable": true
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "confirmations": [
                  {
                    "source_column": "full_name",
                    "target": "person.full_name",
                    "confirmed_by": "qa-runner"
                  },
                  {
                    "source_column": "email",
                    "target": "contact.email",
                    "confirmed_by": "qa-runner"
                  },
                  {
                    "source_column": "street_address",
                    "target": "place.address_line1",
                    "confirmed_by": "qa-runner"
                  },
                  {
                    "source_column": "city",
                    "target": "place.locality",
                    "confirmed_by": "qa-runner"
                  },
                  {
                    "source_column": "country",
                    "target": "place.country",
                    "confirmed_by": "qa-runner"
                  },
                  {
                    "source_column": "external_id",
                    "target": "external.id",
                    "confirmed_by": "qa-runner",
                    "external_system": "partner-extract"
                  }
                ],
                "mapping_template_id": null
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/mapping-suggestions": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Ask for mapping suggestions on the previewed run",
        "description": "Propose a canonical target for each previewed column, with a confidence and a human-readable reason on every suggestion. NOTHING IS APPLIED: the response carries confirmed:false throughout, and the mapping is only stored once a human confirms it through PUT /mapping. An AI assistant, when wired, sees column names, types and REDACTED samples only, may override the deterministic matcher only when it beats its confidence, and its reasons are prefixed \"assistant:\" so a reviewer can tell a model's guess from a rule's match. Address columns propose a place target plus a located_at relationship, never a column on the person. Requires the preview to have run (409 PREVIEW_REQUIRED otherwise). Returns 200: it computes, it does not create. Required: tenant_id.",
        "operationId": "post_api_imports_runs_run_id_mapping_suggestions",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "mapping_suggestion_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "PREVIEW_REQUIRED — run the preview before asking for mapping suggestions"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>"
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/preview": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Preview the uploaded delimited content",
        "description": "Detect delimiter, encoding, header row and per-column type, each with a confidence, and move the run to previewing. Delimiter detection scores CONSISTENCY across lines rather than frequency, so a pipe-delimited file full of commas in its free text is read correctly. Columns holding direct identifiers, contact points, locations, government ids or financial data are flagged for tokenization at trusted ingress AND have their sample values REDACTED — a preview travels into UIs, logs and tickets. Source-identifier columns are detected by name and by all-distinct-uuid shape and reported as crosswalks that are never replaced by platform ids. Send either raw `content` or a pre-parsed `rows[]`. Returns 200: it moves an existing run. Required: tenant_id and one of content / rows[].",
        "operationId": "post_api_imports_runs_run_id_preview",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "preview_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "content": "full_name,email,street_address,city,country,external_id,contact_ok,consent_date\\nAda Lovelace,{{dynamic:email}},1 Analytical Way,London,GB,CRM-{{dynamic:uuid}},yes,2026-01-05T10:00:00Z\\nAlan Turing,{{dynamic:email}},2 Bombe Road,Manchester,GB,CRM-{{dynamic:uuid}},,",
                    "delimiter": ",",
                    "has_header_row": true,
                    "encoding": "utf-8",
                    "sample_size": 200,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — either content or a non-empty rows[] is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "INVALID_RUN_TRANSITION — run <id> cannot move <from> -> previewing"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "content": {
                    "type": "string"
                  },
                  "delimiter": {
                    "type": "string"
                  },
                  "has_header_row": {
                    "type": "boolean"
                  },
                  "encoding": {
                    "type": "string"
                  },
                  "sample_size": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "content": "full_name,email,street_address,city,country,external_id,contact_ok,consent_date\\nAda Lovelace,{{dynamic:email}},1 Analytical Way,London,GB,CRM-{{dynamic:uuid}},yes,2026-01-05T10:00:00Z\\nAlan Turing,{{dynamic:email}},2 Bombe Road,Manchester,GB,CRM-{{dynamic:uuid}},,",
                "delimiter": ",",
                "has_header_row": true,
                "encoding": "utf-8",
                "sample_size": 200
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/rollback": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Roll the committed run back inside its window",
        "description": "Reverse every entity the run created. Permitted only while BOTH hold: the rollback deadline has not passed, and no downstream governed action has touched an affected entity. The second rule protects people rather than data — once a message has gone out against a record this import created, deleting the record does not undo the consequence, it destroys the evidence of it. That refusal is a 409 that NAMES the blocking action, the entity and when it happened, because \"cannot roll back\" with no reason leaves the operator nothing to act on. Refusing changes nothing: the entities and the run status are left exactly as they were. Rolling back an already-rolled-back run is an idempotent no-op. Returns 200. Required: tenant_id.",
        "operationId": "post_api_imports_runs_run_id_rollback",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "wrong file uploaded",
                    "actor_id": "qa-runner",
                    "rollback_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          },
          "409": {
            "description": "INVALID_RUN_TRANSITION — run <id> cannot move <from> -> rolled_back"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason": "wrong file uploaded",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/imports/runs/{run_id}/transform-plan": {
      "post": {
        "tags": [
          "sdk-import"
        ],
        "summary": "Build the transform plan for the confirmed mapping",
        "description": "Build the deterministic transform plan from the confirmed mapping: the same mapping always produces the same steps in the same order, every step says in words what it will do, and every step preserves the raw input as evidence. The plan is built BEFORE identity resolution runs, because reviewing a transform afterwards is reviewing a decision already made. Mapping the source system's own status values onto platform workflow states is a per-tenant business judgement, so that step is present in the plan but DISABLED unless enable_source_state_mapping is true. default_calling_region is what lets a bare national telephone number be normalized at all — without it those rows go to review rather than being guessed into the wrong country. Returns 200: it moves an existing run. Required: tenant_id.",
        "operationId": "post_api_imports_runs_run_id_transform_plan",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "run_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "transform_plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "enable_source_state_mapping": false,
                    "default_calling_region": "44",
                    "default_country": "GB",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "IMPORT_RUN_NOT_FOUND — import run <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "enable_source_state_mapping": {
                    "type": "boolean"
                  },
                  "default_calling_region": {
                    "type": "string"
                  },
                  "default_country": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "enable_source_state_mapping": false,
                "default_calling_region": "44",
                "default_country": "GB"
              }
            }
          }
        }
      }
    },
    "/api/notifications/frequency-policy": {
      "get": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "List all policies for the tenant",
        "description": "Lists the frequency policies visible to a tenant — its own overrides AND the platform defaults they replace, each tagged source: tenant|platform — and, when a channel is named, the RESOLVED policy plus current usage (used_last_24h and remaining). Proves AC1 and AC4. Usage is included because the question a caller actually has is 'may I send now', not 'what rows exist'; making them fetch the policy and then count themselves would duplicate the precedence logic on the client, where it would drift. Returning platform rows alongside tenant ones lets a tenant see what it is overriding and what it would fall back to if the override were removed — the question asked immediately before deleting one. QA edge cases: requiresAuth applies to this GET exactly as to the PUT (MUST-52), so no Bearer is 401; a missing tenant_id is 400; remaining is null when the resolved policy is uncapped, which is distinct from 0 (blocked); usage counts only rows whose outcome is 'sent', so a deduped retry never inflates it — otherwise a retry storm would appear to exhaust the allowance without a single extra message having been delivered; purpose defaults to the catch-all '*' when omitted.",
        "operationId": "get_api_notifications_frequency_policy",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "frequency_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Cap marketing SMS at 5 a day with a 15-minute dedup window",
        "description": "Upserts a tenant's frequency policy for one channel + purpose (either may be '*', the catch-all). An upsert, so 200 rather than 201. Proves AC1 and AC4. Two distinct settings, deliberately not merged: max_per_day is a RATE answered by counting, and dedup_window_seconds is an IDENTITY answered by a unique key — counting cannot express dedup (two retries are two rows, so they would burn two units of the cap and still both be delivered) and a unique key cannot express a rate, so conflating them produces a system that is wrong in one direction or the other. Caps are evaluated per channel AND per purpose (AC4), so an OTP is never throttled because marketing exhausted the allowance. QA edge cases: max_per_day null means UNCAPPED and is explicitly NOT the same as 0, which blocks everything — the shipped platform default is uncapped precisely because turning caps on for every existing tenant during a migration would silently start dropping their traffic, the one outcome an additive change must not cause; a negative max_per_day or a dedup window outside 0..604800 (7 days) is a 400; the PLATFORM row (tenant_id NULL) is unreachable from this endpoint, since a tenant editing the shared default would change every other tenant's limits; precedence on read is tenant before platform and then most-specific-first, but a tenant catch-all still beats a platform-specific rule because the tenant has stated a house policy.",
        "operationId": "put_api_notifications_frequency_policy",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "frequency_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "channel": "sms",
                    "purpose": "marketing",
                    "max_per_day": 5,
                    "dedup_window_seconds": 900,
                    "updated_by": "api-regression",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — max_per_day must be a non-negative integer, or null for uncapped"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "max_per_day": {
                    "type": "integer"
                  },
                  "dedup_window_seconds": {
                    "type": "integer"
                  },
                  "updated_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "sms",
                "purpose": "marketing",
                "max_per_day": 5,
                "dedup_window_seconds": 900,
                "updated_by": "api-regression"
              }
            }
          }
        }
      }
    },
    "/api/parsing/contact/extract-batch": {
      "post": {
        "tags": [
          "sdk-parsing"
        ],
        "summary": "Two valid captures both extract",
        "description": "Runs contact extraction over up to 100 captures in one request, each with its own source_kind and raw. An action endpoint returning 200; nothing is persisted. Per-item isolation is the point: ONE malformed item does not fail the batch, because a caller pasting forty signatures should not lose thirty-nine to one bad entry. Each entry returns {id, ok, result|error} and the id is echoed back so a caller correlates by id rather than relying on array order, then retries exactly the failures. The response is 200 even when some items failed — the batch itself succeeded, and a 4xx would hide the items that extracted cleanly, so read ok_count/failed_count rather than the status code. allow_llm applies to the whole batch and defaults to opt-OUT exactly as on the single endpoint (AC1); every proposal carries confidence and a verified evidence span (AC2). QA edge cases: an empty items array is a 400 rather than a meaningless success; more than 100 items is a 400 naming the cap, so a caller learns the limit instead of silently having the tail dropped; per-item validation failures are reported with the offending INDEX (items[1].source_kind ...) so they can be found in a large payload; items[].raw is required for every item for the same evidence-span reason as the single endpoint.",
        "operationId": "post_api_parsing_contact_extract_batch",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "extract_batch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "items": [
                      {
                        "id": "a",
                        "source_kind": "SMART_PASTE",
                        "raw": "jane@acme.com"
                      },
                      {
                        "id": "b",
                        "source_kind": "EMAIL_SIGNATURE",
                        "raw": "Bob Smith\nbob@acme.com"
                      }
                    ],
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — items must be a non-empty array"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "source_kind": {
                          "type": "string"
                        },
                        "raw": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "items": [
                  {
                    "id": "a",
                    "source_kind": "SMART_PASTE",
                    "raw": "jane@acme.com"
                  },
                  {
                    "id": "b",
                    "source_kind": "EMAIL_SIGNATURE",
                    "raw": "Bob Smith\nbob@acme.com"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/parsing/contact/extract": {
      "post": {
        "tags": [
          "sdk-parsing"
        ],
        "summary": "Email signature yields multiple qualified handles",
        "description": "Extracts contact-field PROPOSALS from one captured input. An action endpoint, so 200 rather than 201 — nothing is persisted; extraction guesses and a human confirms, which is why the response is proposals and never a finished contact. Eight capture surfaces are supported via source_kind: SMART_PASTE, EMAIL_SIGNATURE, BUSINESS_CARD_OCR, VCARD, VCARD_MULTI, MOBILE_CONTACTS, BROWSER_SELECTION, VOICE_TRANSCRIPT. Proves all four criteria. AC1: the deterministic local parser ALWAYS runs first and unconditionally, and the sdk-ai-gateway LLM adjunct is reached only when required fields remain unresolved after it AND allow_llm is explicitly true — an absent allow_llm is opt-OUT, because sending tenant text to a model must be the caller's decision, not this SDK's; llm_reason is always populated so a skip is auditable rather than invisible. AC2: every proposal carries confidence 0..1 plus an evidence span {start,end,snippet} indexing into raw. AC3: nothing is fabricated — each proposal is re-verified by slicing its span out of raw and confirming the normalised slice contains the normalised value; failures are DROPPED into rejected[] with a stated reason, never silently kept, and the guard applies to LLM output identically, which is what stops a model's plausible-but-absent company name from reaching the caller. AC4: the schema resolves tenant-first then platform via sdk-taxonomy lookupExtractionSchema, with a builtin last resort so a fresh install still extracts; schema.source reports which of the three answered. QA edge cases: raw is required even for MOBILE_CONTACTS, because evidence spans index into it and without it the guard would be blind — the orchestrator serialises the structured payload to JSON and passes that as raw, so device contacts are verified by the same rule as everything else; values are returned VERBATIM, since a phone reformatted to E.164 would no longer appear in the source and would be indistinguishable from an invented one; a taxonomy outage falls back to the builtin rather than failing the paste; VOICE_TRANSCRIPT reconstructs 'x at y dot com' into an address but its evidence points at the ORIGINAL spoken characters and confidence is capped well below typed input; EMAIL_SIGNATURE keeps every handle and qualifies them work/mobile/home/fax rather than collapsing to one, and offers the email domain only as org_candidate — a domain is evidence of a company, not its name — never promoted to organization; BUSINESS_CARD_OCR scales every confidence down because the same regex hit is genuinely less trustworthy on OCR output than on pasted text.",
        "operationId": "post_api_parsing_contact_extract",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "extract_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "source_kind": "EMAIL_SIGNATURE",
                    "raw": "Jane Okonkwo\nHead of Platform Engineering\nAcme Technologies Ltd\nWork: +44 20 7946 0958\nMobile: 07700 900123\njane.okonkwo@acme-tech.com",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — raw is required — evidence spans index into it"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "source_kind": {
                    "type": "string"
                  },
                  "raw": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "source_kind": "EMAIL_SIGNATURE",
                "raw": "Jane Okonkwo\nHead of Platform Engineering\nAcme Technologies Ltd\nWork: +44 20 7946 0958\nMobile: 07700 900123\njane.okonkwo@acme-tech.com"
              }
            }
          }
        }
      }
    },
    "/api/parsing/contact/schemas": {
      "get": {
        "tags": [
          "sdk-parsing"
        ],
        "summary": "Resolve the contact schema for a tenant",
        "description": "Returns the contact extraction schema that WOULD be applied for this tenant, plus the list of supported capture surfaces. Proves AC4. Resolution is tenant-first with platform fallback, delegated to the existing sdk-taxonomy lookupExtractionSchema — whose query already ranks tenant versions above platform ones via ORDER BY (v.tenant_id IS NOT NULL) DESC — rather than reimplemented here, since a second resolver would be a second thing to keep correct. A builtin field set is the last resort so a fresh install can extract contacts before anyone has seeded a taxonomy; failing hard there would make the feature undemonstrable on day one. schema.source is returned as tenant|platform|builtin so a caller can TELL which of the three answered instead of inferring it from the field list. source_kinds is returned so a client builds its capture UI from the server response rather than hard-coding a list that drifts the moment a backend is registered. QA edge cases: requiresAuth applies to this GET exactly as to the POSTs (MUST-52), so no Bearer is 401; a missing tenant_id is 400 because without it the tenant-first half of the resolution is meaningless; a taxonomy outage returns the builtin with source='builtin' and a 200 rather than a 5xx, matching the extract endpoint's degrade-don't-fail behaviour; taxonomy_version_id is optional and is only echoed back on the builtin path.",
        "operationId": "get_api_parsing_contact_schemas",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "schema_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/payments/provider": {
      "get": {
        "tags": [
          "sdk-payment"
        ],
        "summary": "Resolve the tenant's payment collection provider",
        "description": "Two-level payment-provider resolution (EP-341) via the config plane. level='collect' (default) resolves payment.provider along the caller's tenant->platform chain (how a tenant collects from its end-users); level='billing' resolves platform-scope only (how the tenant pays ProjexLight, no tenant override). Returns 200 { data: { level, configured, provider, scope, value } } — configured=false with nulls when no provider is set at any scope in range. Tenant JWT required.",
        "operationId": "get_api_payments_provider",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "provider_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/projection/replay": {
      "post": {
        "tags": [
          "sdk-projection"
        ],
        "summary": "Replay a single subject",
        "description": "Rebuilds a subject's projection FROM the assertion log rather than patching it, and persists a snapshot plus a content hash. An action endpoint and idempotent, so 200 on the first call and on every repeat — a repeat is expected, not an error. Proves all four criteria. AC1: patching would require knowing what a retracted assertion contributed, i.e. trusting a delta computed against state you no longer hold, after which two patches applied in a different order disagree and nothing can say which is right; a replay derives the answer from the surviving assertions, so the result depends only on the log's CONTENT and never on the path taken — deterministic and idempotent are the same property here. The content_hash is taken over a CANONICAL projection with the wall-clock stamp stripped, so two identical replays hash equal instead of looking like a change; changed=false on a repeat. AC2: retract_assertion_id and supersede_assertion_id retract/link AND replay the affected subject in the SAME call, so a successful retraction has by definition already propagated — a scheduled follow-up would leave a window in which the projection still shows a formally withdrawn value, and that window is exactly when someone reads it. AC3: every replay appends projection.replay.completed.v1 to the audit chain with both hashes, and a retraction additionally appends projection.assertion.retracted.v1; a no-op replay is STILL recorded, because 'we replayed and it made no difference' is itself the auditable answer. AC4: measured, not asserted — a 10,000-assertion subject replays in ~450-550ms against a 3000ms budget, twice, with an identical hash. QA edge cases: the snapshot is a CACHE plus evidence, never a source of truth — deleting it and replaying reproduces the same hash exactly, which is the property that makes the rebuild trustworthy; scope=tenant is BOUNDED and returns `remaining` rather than sweeping unbounded, so a rule edit on a million-subject tenant cannot become an open-ended synchronous job, and silent truncation would wrongly read as 'done'; retracting or superseding an unknown assertion is 404 rather than a fake success; an assertion may not supersede itself; superseded_by is required whenever supersede_assertion_id is given; a tenant-wide replay emits ONE summary event instead of N, since each subject's snapshot already records its own hash and 900 events would bury the ledger.",
        "operationId": "post_api_projection_replay",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "status": "completed",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "lead:regression-subject",
                    "trigger": "manual",
                    "reason": "api regression",
                    "replay_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — subject_ref, retract_assertion_id or supersede_assertion_id is required for scope=subject"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "ASSERTION_NOT_FOUND — NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "trigger": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "subject_ref": "lead:regression-subject",
                "trigger": "manual",
                "reason": "api regression"
              }
            }
          }
        }
      }
    },
    "/api/projection/subject/{subject_ref}/explained": {
      "get": {
        "tags": [
          "sdk-projection"
        ],
        "summary": "Explained projection for a subject",
        "description": "Returns, per attribute, the surviving value AND every losing assertion with a concrete reason it lost. Proves AC1, AC3 and AC4. AC1: the reason is a full sentence naming the deciding criterion, both compared values and the criterion's position in the rule order — e.g. \"lost on origin_class (criterion 2 of 4): 'import' ranks below 'user_supplied' in this tenant's order [human_verified > user_supplied > import]\" — never a bare status word like superseded or stale, because a status word tells a user nothing they can act on: they cannot see whether the import beat their correction through precedence they would agree with, or because someone set a confidence wrong. decided_by carries the same facts structurally for callers that would rather format their own text. Only the FIRST separating criterion is reported, because later criteria were genuinely never consulted. AC3: losing is COMPUTED on read, never written — no column changes on a losing row, so the same rows re-explain themselves under a changed rule set and a rule change is a projection change rather than a data migration; losers stay ordinary queryable rows. AC4: the ordering is total, with a final assertion_id tie-break, so repeated calls on identical inputs return byte-identical output apart from projected_at; without that tie-break two assertions equal on every criterion could come back in either order depending on how Postgres returned rows, and stability would hold only by luck. Attributes are returned sorted for the same reason. QA edge cases: subject_ref is URL-decoded because refs are '<kind>:<id>' and the colon is escaped in a path segment; retracted and rejected assertions are excluded from the CONTEST by default (a retracted claim should not win) but are counted in excluded_count and remain retrievable via include_retracted=true or include_all_assertions=true, so nothing is hidden either way; an exact tie on every criterion says so explicitly and names the tie-break rather than pretending it was decided on merit, which is usually the signal a rule set needs another criterion; an unknown origin_class sorts last instead of throwing, so a new source appearing in the data cannot break the whole projection; a subject with no assertions returns 200 with an empty attributes array rather than 404, since 'no data yet' is not an error.",
        "operationId": "get_api_projection_subject_subject_ref_explained",
        "parameters": [
          {
            "name": "subject_ref",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "subject_ref path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "explained_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/projection/survivorship-rules": {
      "get": {
        "tags": [
          "sdk-projection"
        ],
        "summary": "List rule sets for the tenant",
        "description": "Lists the survivorship rule sets visible to a tenant — its own overrides AND the platform defaults they replace — each tagged with source: 'tenant' or 'platform'. Proves AC2. Returning both is deliberate: a tenant that only saw its own rows could not tell what it is overriding, nor what it would fall back to if it deleted an override, which is exactly the question someone asks before removing one. Tenant rows sort first, then by attribute, so the response order is stable. QA edge cases: requiresAuth applies to this GET exactly as to the PUT (MUST-52), so no Bearer is 401; a missing tenant_id is 400, since without it the tenant half of the listing is undefined; a tenant with no overrides still gets a 200 listing the platform rows rather than an empty array, because 'no overrides' and 'no rules at all' are different situations and an empty list would imply the latter; the platform row is read-only through this API and is never returned as editable; source is the only reliable way to distinguish the two, since a tenant override and the default share their shape by design.",
        "operationId": "get_api_projection_survivorship_rules",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "survivorship_rule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "put": {
        "tags": [
          "sdk-projection"
        ],
        "summary": "Set a catch-all precedence for the tenant",
        "description": "Upserts a tenant's survivorship rule set for one attribute (or '*', the catch-all). An upsert, so 200 rather than 201 — the same call creates or replaces. Proves AC2. criteria is an ORDERED array, not a set of weights: order IS the rule, and 'a verified value beats an unverified one, and only if that ties does origin matter' cannot be expressed as weights without inventing magic numbers — worse, weights make a loss unexplainable ('it scored 0.62') where an ordered list makes it a sentence. Valid criteria are verification_state, origin_class (both requiring a non-empty 'order' array, best first) and confidence, recency (both taking an optional direction, default desc). Resolution elsewhere is tenant-first then platform then a code builtin. QA edge cases: the PLATFORM row (tenant_id NULL) is deliberately unreachable from this endpoint — a tenant editing the shared default would change every other tenant's precedence, so a tenant overrides by writing its own row and the platform row is never edited in place, which keeps 'what does the platform say' answerable; a repeated criterion is a 400 rather than being accepted, because the second occurrence can never be reached and silently accepting it would leave the author believing it applies; duplicate entries inside an 'order' array are a 400 since their precedence would be ambiguous; validation happens at WRITE time on purpose — a bad rule discovered during a projection produces a wrong winner nobody notices, whereas the same rule refused at PUT produces an error the author can act on; an attribute-specific tenant rule beats the tenant catch-all, but a tenant catch-all still beats a platform-specific rule, because the tenant has deliberately stated a house policy.",
        "operationId": "put_api_projection_survivorship_rules",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "survivorship_rule_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "attribute": "*",
                    "criteria": [
                      {
                        "criterion": "verification_state",
                        "order": [
                          "verified",
                          "unverified",
                          "rejected"
                        ]
                      },
                      {
                        "criterion": "origin_class",
                        "order": [
                          "human_verified",
                          "user_supplied",
                          "import"
                        ]
                      },
                      {
                        "criterion": "confidence",
                        "direction": "desc"
                      },
                      {
                        "criterion": "recency",
                        "direction": "desc"
                      }
                    ],
                    "updated_by": "api-regression",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — criteria[0].criterion must be one of: verification_state, origin_class, confidence, recency"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "attribute": {
                    "type": "string"
                  },
                  "criteria": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "criterion": {
                          "type": "string"
                        },
                        "order": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "updated_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "attribute": "*",
                "criteria": [
                  {
                    "criterion": "verification_state",
                    "order": [
                      "verified",
                      "unverified",
                      "rejected"
                    ]
                  },
                  {
                    "criterion": "origin_class",
                    "order": [
                      "human_verified",
                      "user_supplied",
                      "import"
                    ]
                  },
                  {
                    "criterion": "confidence",
                    "direction": "desc"
                  },
                  {
                    "criterion": "recency",
                    "direction": "desc"
                  }
                ],
                "updated_by": "api-regression"
              }
            }
          }
        }
      }
    },
    "/api/relationships/roles": {
      "get": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Live roles for a pair",
        "description": "Lists the contextual roles held by a persona, filtered by counterparty, kind, label or trust_state. Proves AC1 (several roles coexist per pair, each with its own trust and validity) and AC2 (closed roles are still retrievable). The as_of parameter is the bitemporal read — valid_from <= t AND (valid_to IS NULL OR valid_to > t) — which lets 'who was the carer last March' be answered from the SAME rows that answer 'who is the carer now'. That is the point of storing validity on the row: no snapshot table, no parallel history table, and therefore no way for the two to disagree. QA edge cases: the default view is live-as-of-now, so a closed role disappears from it while remaining fully retrievable with include_closed=true — losing is a date on the row, never a deletion; as_of correctly excludes a role whose valid_from is later than the instant asked about, which is what distinguishes a real bitemporal read from a naive 'not closed yet' filter; requiresAuth applies to this GET exactly as to the POST (MUST-52); a missing persona_a is 400 since the query would otherwise span every persona; limit is clamped to 1..1000; results are ordered by valid_from DESC then relationship_id so the response is stable across repeated reads.",
        "operationId": "get_api_relationships_roles",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "role_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — persona_a query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Grant a DOCUMENTED carer role with evidence",
        "description": "Grants one contextual role between two personas. A collection-root create, so 201. Proves AC1 and AC3. A subject-object pair is no longer ONE relationship: someone can be a patient's daughter AND registered carer AND billing contact simultaneously, each with its own role_label, its own trust_state and its own valid_from/valid_to — collapsing them into a single edge would force a choice between losing the distinction and overwriting a role that is still true. TRUST AND VALIDITY ARE SEPARATE AXES on purpose: 'is this in force now' and 'how sure are we it is real' have different remedies (an expired carer role needs renewing, an unevidenced one needs a document), and a single status field answers 'inactive' to both. AC3: CONFIRMED and DOCUMENTED require at least one evidence_ref, enforced BOTH in the service (readable error) and by a CHECK constraint (so a backfill or direct insert cannot slip past); CANDIDATE requires none, which is also why it is the column default — any other default would have invalidated every pre-existing row the moment the column was added. QA edge cases: the evidence CHECK uses cardinality() rather than array_length(), because array_length('{}',1) returns NULL and a NULL CHECK PASSES — written the obvious way the constraint silently permits the exact row it exists to forbid; whitespace-only evidence refs are stripped and then count as absent; a persona cannot hold a role to itself; a duplicate LIVE role for the same (pair, kind, role_label) is a 409, but the same pair may hold unlimited DIFFERENT labels and unlimited CLOSED historical rows; the live-role uniqueness is scoped to labelled rows only, so pre-existing unlabelled edges — which the original schema legitimately permits in duplicate — keep working unchanged.",
        "operationId": "post_api_relationships_roles",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "role_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "kind": "care-team",
                    "persona_a": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "persona_b": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "role_label": "registered_carer",
                    "trust_state": "DOCUMENTED",
                    "evidence_refs": [
                      "doc:poa-77"
                    ],
                    "valid_from": "2026-01-15T10:30:00Z",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "EVIDENCE_REQUIRED — [sdk-rebac] trust_state 'CONFIRMED' requires at least one evidence_ref"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "409": {
            "description": "ROLE_ALREADY_LIVE — a live role with this kind and label already exists for the pair"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "persona_a": {
                    "type": "string"
                  },
                  "persona_b": {
                    "type": "string"
                  },
                  "role_label": {
                    "type": "string"
                  },
                  "trust_state": {
                    "type": "string"
                  },
                  "evidence_refs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "valid_from": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "kind": "care-team",
                "persona_a": "<id from personas>",
                "persona_b": "<id from personas>",
                "role_label": "registered_carer",
                "trust_state": "DOCUMENTED",
                "evidence_refs": [
                  "doc:poa-77"
                ],
                "valid_from": "<generated pastdatetime>"
              }
            }
          }
        }
      }
    },
    "/mcp/v1/call": {
      "post": {
        "tags": [
          "registry-mcp"
        ],
        "summary": "Invoke a read tool and receive an MCP content envelope",
        "description": "Single dispatch entry point for the registry MCP service: the caller names a tool and passes its arguments, and the service routes to either a read dispatcher or a write dispatcher depending on whether the name is in the write-tool set. Authentication accepts EITHER an Authorization bearer token or an x-projex-api-key header; tenant context is derived from whichever is present, and the resolved tenant is also the rate-limit key. Note that a failing TOOL still returns HTTP 200 — the failure is carried in the body as isError=true with the detail in content[0].text, because the HTTP call itself succeeded. Only transport-level problems (auth, rate limit, malformed request) produce non-2xx. Every invocation is audited with the tool name, duration and error code.",
        "operationId": "post_mcp_v1_call",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "call_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "name": "list_sdks",
                    "arguments": {},
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "ValidationError — name is required"
          },
          "401": {
            "description": "Unauthorized — missing Authorization header or x-projex-api-key"
          },
          "429": {
            "description": "RateLimited — rate limited"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "arguments": {
                    "type": "object",
                    "properties": {}
                  }
                }
              },
              "example": {
                "name": "list_sdks",
                "arguments": {}
              }
            }
          }
        }
      }
    },
    "/api/sla/at-risk": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Everything due within two hours, overdue included",
        "description": "Live clocks approaching or past their deadline, ordered by how close that deadline is — a queue that does not put the next breach first is a list rather than a queue. Each row carries minutes_to_due, is_overdue, how many rungs have already fired, the highest severity fired so far and the NEXT rung with the instant it is due. at_risk is derived here and never stored: keeping a column truthful minute by minute would need a background job, and a stale \"at risk\" flag is worse than none. Returns 200 with at_risk and a count. Requires tenant_id.",
        "operationId": "get_api_sla_at_risk",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "at_risk_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/attainment": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Attainment for the last thirty days across every dimension",
        "description": "How often the promise was kept over a window, how long it actually took, and for every miss why and what was done. Percentage attainment plus median and P95 over BUSINESS minutes on each policy's own calendar — the same measure and arithmetic that produced due_at, so a percentile and a deadline can never describe different clocks. Broken down by source, owner, day, hour, reason and policy (pick with ?dimensions=, and an unrecognised dimension is refused rather than dropped, because silently returning fewer breakdowns than were asked for reads as \"no data\"). Each bucket carries its own misses with cause and recovery, so a row can be read without going back to the source. Only CLOSED clocks count: an open clock has no outcome and scoring it either way would be a guess. A breached clock whose cause nobody recorded appears with reason_code null, grouped as cause_not_recorded and counted in misses_without_cause — named rather than dropped, because a report that quietly omits the misses nobody explained flatters itself. If the clock ceiling is reached the response says so via truncated and clocks_considered instead of capping silently. Returns 200. Required: tenant_id, from, to.",
        "operationId": "get_api_sla_attainment",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "attainment_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, from and to query params are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/breach-reasons": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the taxonomy",
        "description": "The tenant's cause taxonomy in code order, including which codes were auto-registered from use rather than defined deliberately — the list an operator prunes. Returns 200 with reasons and a count. Requires tenant_id.",
        "operationId": "get_api_sla_breach_reasons",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "breach_reason_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Give the auto-registered code a label and a category",
        "description": "Define or relabel a cause code in the tenant's taxonomy. The taxonomy is per tenant and NOT a platform enum: one shared vocabulary for why every vertical missed would be exactly the kind of business rule this package must not hold. Naming a code deliberately clears the is_auto_registered flag it got from first use, which is how an operator distinguishes a vocabulary that was decided from one that accumulated. An upsert, so 200 rather than 201. Required: tenant_id, code.",
        "operationId": "post_api_sla_breach_reasons",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "breach_reason_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "code": "no_capacity",
                    "label": "No capacity available",
                    "category": "capacity",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and code are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "code": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "category": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "code": "no_capacity",
                "label": "No capacity available",
                "category": "capacity"
              }
            }
          }
        }
      }
    },
    "/api/sla/breach-scan": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Scan the tenant — the ten-day-old clock is well past due",
        "description": "Move every past-due running clock to breached. This asserts the ARITHMETIC only — the deadline passed — and deliberately does NOT invent a reason code: if the scanner had to produce a cause it would have to make one up, and an invented cause is worse than a missing one because it looks like an answer. The response therefore reports awaiting_cause, the count of breached clocks nobody has explained yet, as visible debt to be cleared through POST /api/sla/clocks/:clock_id/breach. Idempotent — the update only matches state=running, so a second scan in the same second marks nothing twice and emits nothing twice. Returns 200 with the counters. Required: tenant_id.",
        "operationId": "post_api_sla_breach_scan",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "breach_scan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "limit": 200,
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "limit": {
                    "type": "integer"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "limit": 200,
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/breaches/{breach_id}": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the breach recorded above",
        "description": "One breach record: the cause and its detail, elapsed and overdue business minutes, the owner and source at breach time, whether it was flagged systemic, and the recovery if one has been recorded. Returns 200, or 404 SLA_BREACH_NOT_FOUND outside the tenant.",
        "operationId": "get_api_sla_breaches_breach_id",
        "parameters": [
          {
            "name": "breach_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "breach_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "breach_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_BREACH_NOT_FOUND — breach record <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/breaches/{breach_id}/recovery": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "The backup answered the next morning",
        "description": "Record the recovery action and the recovering persona after the fact. The cause stays exactly as recorded — a trigger refuses any change to reason_code, breached_at or the clock reference, because an attainment history that rewrites its own causes cannot be used to argue for anything, whereas recovery genuinely happens later. Returns 200 with the updated record; 404 SLA_BREACH_NOT_FOUND outside the tenant. Required: tenant_id, recovery_action.",
        "operationId": "post_api_sla_breaches_breach_id_recovery",
        "parameters": [
          {
            "name": "breach_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "breach_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "recovery_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "recovery_action": "backup owner answered at opening and apologised for the delay",
                    "recovered_by": "persona:backup",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and recovery_action are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_BREACH_NOT_FOUND — breach record <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "recovery_action": {
                    "type": "string"
                  },
                  "recovered_by": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "recovery_action": "backup owner answered at opening and apologised for the delay",
                "recovered_by": "persona:backup"
              }
            }
          }
        }
      }
    },
    "/api/sla/breaches": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "List every recorded miss for this tenant",
        "description": "Recorded misses, most recent first, narrowed by policy, owner, reason_code, window, or unrecovered_only — that last filter being the working queue: misses nobody has said what they did about yet. Returns 200 with breaches and a count. Requires tenant_id.",
        "operationId": "get_api_sla_breaches",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "breach_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/calendars/{calendar_id}": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the calendar created above",
        "description": "Read one business calendar scoped to the tenant, including its working windows, holiday dates, weekend rule and late-coverage extension. Returns 200, or 404 SLA_CALENDAR_NOT_FOUND when the calendar does not belong to this tenant — the same answer as a calendar that does not exist, so the endpoint cannot be used to probe another tenant.",
        "operationId": "get_api_sla_calendars_calendar_id",
        "parameters": [
          {
            "name": "calendar_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "calendar_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_CALENDAR_NOT_FOUND — calendar <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/calendars": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "List the active calendars for this tenant",
        "description": "List the tenant's business calendars in slug order, optionally filtered by is_active. Returns 200 with calendars and a count. Requires the tenant_id query param — a calendar list without a tenant would cross a tenant boundary.",
        "operationId": "get_api_sla_calendars",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "A Monday-to-Friday calendar in a named zone with a late-coverage extension",
        "description": "Create the business calendar a promise is measured against: a NAMED IANA timezone (never a fixed UTC offset — the constraint and the service both refuse \"+05:30\" or \"UTC-5\", because an offset cannot know about DST and a due date computed from one is wrong twice a year), per-weekday working windows keyed 1=Monday..7=Sunday, an optional late-coverage extension that keeps a signal arriving one minute before close due the same evening rather than deferring to tomorrow, a weekend rule and a holiday date list. Returns 201 with the stored calendar. Required: tenant_id, slug, name, timezone and a non-empty working_windows — a calendar with no open minute can never produce a due date, so it is refused at creation rather than at the first clock.",
        "operationId": "post_api_sla_calendars",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "support-hours-{{dynamic:uuid}}",
                    "name": "Support hours",
                    "description": "Weekday cover with a half-hour of late coverage",
                    "timezone": "America/New_York",
                    "working_windows": {
                      "1": [
                        {
                          "start": "09:00",
                          "end": "17:00"
                        }
                      ],
                      "2": [
                        {
                          "start": "09:00",
                          "end": "17:00"
                        }
                      ],
                      "3": [
                        {
                          "start": "09:00",
                          "end": "17:00"
                        }
                      ],
                      "4": [
                        {
                          "start": "09:00",
                          "end": "17:00"
                        }
                      ],
                      "5": [
                        {
                          "start": "09:00",
                          "end": "17:00"
                        }
                      ]
                    },
                    "late_coverage_extension_minutes": 30,
                    "weekend_rule": "saturday_sunday",
                    "holiday_dates": [
                      "2026-12-25"
                    ],
                    "metadata": {
                      "owner": "qa-runner"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, slug, name and timezone are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "422": {
            "description": "FIXED_OFFSET_TIMEZONE_REJECTED — timezone '+05:30' is a fixed offset, not a zone"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "working_windows": {
                    "type": "object",
                    "properties": {
                      "1": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "start": {
                              "type": "string"
                            },
                            "end": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "2": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "start": {
                              "type": "string"
                            },
                            "end": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "3": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "start": {
                              "type": "string"
                            },
                            "end": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "4": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "start": {
                              "type": "string"
                            },
                            "end": {
                              "type": "string"
                            }
                          }
                        }
                      },
                      "5": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "start": {
                              "type": "string"
                            },
                            "end": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  },
                  "late_coverage_extension_minutes": {
                    "type": "integer"
                  },
                  "weekend_rule": {
                    "type": "string"
                  },
                  "holiday_dates": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "owner": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "slug": "support-hours-{{dynamic:uuid}}",
                "name": "Support hours",
                "description": "Weekday cover with a half-hour of late coverage",
                "timezone": "America/New_York",
                "working_windows": {
                  "1": [
                    {
                      "start": "09:00",
                      "end": "17:00"
                    }
                  ],
                  "2": [
                    {
                      "start": "09:00",
                      "end": "17:00"
                    }
                  ],
                  "3": [
                    {
                      "start": "09:00",
                      "end": "17:00"
                    }
                  ],
                  "4": [
                    {
                      "start": "09:00",
                      "end": "17:00"
                    }
                  ],
                  "5": [
                    {
                      "start": "09:00",
                      "end": "17:00"
                    }
                  ]
                },
                "late_coverage_extension_minutes": 30,
                "weekend_rule": "saturday_sunday",
                "holiday_dates": [
                  "2026-12-25"
                ],
                "metadata": {
                  "owner": "qa-runner"
                }
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/breach": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Explain the miss: nobody was available on the roster",
        "description": "Record a missed promise. reason_code is MANDATORY — a blank or absent code returns 422 BREACH_REASON_REQUIRED, because a miss with no stated cause is a number on a dashboard nobody can act on. Unknown codes are auto-registered into the tenant's cause taxonomy and flagged, so a breach is never lost to an unconfigured vocabulary while an operator can still see which codes grew by accident. Elapsed and overdue are measured in BUSINESS minutes on the policy calendar, net of paused intervals. Idempotent per clock: one clock misses its deadline once, and a retried call returns the record that already exists with created:false rather than a second one that would double-count in every report. With is_systemic the breach joins a GROUP (policy + cause + hour by default, or your systemic_group_key) and ONLY the call that creates the group opens an incident — so a ladder that fired four rungs, or twenty clocks failing for one reason, still produce exactly one. Returns 200. Required: tenant_id and reason_code.",
        "operationId": "post_api_sla_clocks_clock_id_breach",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "breach_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason_code": "no_capacity",
                    "reason_detail": "roster gap overnight, nobody on call for this queue",
                    "source_ref": "web_form",
                    "is_systemic": false,
                    "recorded_by": "persona:manager",
                    "metadata": {
                      "reviewed": true
                    },
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_CLOCK_NOT_FOUND — clock <id> not found for tenant"
          },
          "409": {
            "description": "CLOCK_NOT_BREACHED — clock <id> is 'running' and not past due"
          },
          "422": {
            "description": "BREACH_REASON_REQUIRED — a breach cannot be recorded without a reason_code"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason_code": {
                    "type": "string"
                  },
                  "reason_detail": {
                    "type": "string"
                  },
                  "source_ref": {
                    "type": "string"
                  },
                  "is_systemic": {
                    "type": "boolean"
                  },
                  "recorded_by": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "reviewed": {
                        "type": "boolean"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason_code": "no_capacity",
                "reason_detail": "roster gap overnight, nobody on call for this queue",
                "source_ref": "web_form",
                "is_systemic": false,
                "recorded_by": "persona:manager",
                "metadata": {
                  "reviewed": true
                },
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/cancel": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Cancel because the subject withdrew",
        "description": "Cancel a clock with a mandatory reason — the subject withdrew, the request was a duplicate, the promise no longer applies. Any non-terminal clock can be cancelled (running, paused or breached); a satisfied or already-cancelled one returns 409 INVALID_CLOCK_TRANSITION. Returns 200. MANUAL: cancel ends the clock's life, and the create->capture chain in this suite yields exactly one clock_id which the satisfy path needs alive, so an automated case here would either destroy that chain or point at a fabricated id and fail as a 404 while proving nothing. Covered by packages/sdk-sla/tests/clock.integration.test.ts. Required: tenant_id, reason.",
        "operationId": "post_api_sla_clocks_clock_id_cancel",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "cancel_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "subject withdrew the request",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and reason are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "INVALID_CLOCK_TRANSITION — clock <id> cannot move satisfied -> cancelled"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason": "subject withdrew the request",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/firings": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the ledger for the ticked clock",
        "description": "The ledger for one clock: which rungs fired, when they were DUE versus when they actually went (so lateness is visible), how many attempts each took, who the audience resolved to at fire time, the action result and the last error on anything that failed. One row per rung per clock, forever — that uniqueness is what makes exactly-once true. Returns 200 with firings and a count.",
        "operationId": "get_api_sla_clocks_clock_id_firings",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "firing_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/clocks/{clock_id}": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the overdue clock created above",
        "description": "Read one clock, plus the answer to the question every caller asks next: elapsed_business_minutes — how long this has been waiting in the hours the business is actually open, net of any paused intervals — alongside the policy duration it is measured against and whether it is already overdue. Computing that from the raw columns needs the calendar, so it is returned here rather than left to the caller. Returns 200, or 404 SLA_CLOCK_NOT_FOUND outside the tenant.",
        "operationId": "get_api_sla_clocks_clock_id",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "clock_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_CLOCK_NOT_FOUND — clock <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/clocks/{clock_id}/pause": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Pause on the one reason this policy allows",
        "description": "Pause the clock so time spent waiting on somebody else does not burn the responder's promise. The reason MUST appear in the policy's pause_conditions, otherwise 422 PAUSE_REASON_NOT_ALLOWED naming what is allowed — an unconstrained pause button is how a breach becomes invisible, and each listed condition also carries a max_minutes cap. Pausing does not move due_at: the deadline is the promise that was made, and paused time explains a miss rather than excusing it, while elapsed business minutes exclude the parked interval. Only a running clock can pause; anything else is 409. Returns 200. Required: tenant_id, reason.",
        "operationId": "post_api_sla_clocks_clock_id_pause",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "pause_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "reason": "awaiting_subject_reply",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and reason are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "INVALID_CLOCK_TRANSITION — clock <id> cannot move paused -> paused"
          },
          "422": {
            "description": "PAUSE_REASON_NOT_ALLOWED — 'because_i_said_so' is not a pause condition on this policy"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "reason": "awaiting_subject_reply",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/reassign": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Hand the overdue request to the backup owner",
        "description": "Hand the response to somebody else — the reassignment and backup-takeover path. It exists as a named operation precisely to make the safe change easy, so nobody reaches for an UPDATE that would also \"helpfully\" refresh the due date: source_timestamp, started_at and due_at are immutable and a database trigger rejects any attempt to move them. Ownership is fluid, timing is frozen. Returns 200 with the updated clock; 409 INVALID_CLOCK_TRANSITION on a clock that is already closed. Required: tenant_id, owner_ref.",
        "operationId": "post_api_sla_clocks_clock_id_reassign",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "reassign_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "owner_ref": "persona:backup-{{dynamic:uuid}}",
                    "reason": "backup takeover",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and owner_ref are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_CLOCK_NOT_FOUND — clock <id> not found for tenant"
          },
          "409": {
            "description": "INVALID_CLOCK_TRANSITION — clock <id> cannot move satisfied -> satisfied"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "owner_ref": {
                    "type": "string"
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "owner_ref": "persona:backup-{{dynamic:uuid}}",
                "reason": "backup takeover",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/resume": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Resume the paused clock",
        "description": "Resume a paused clock, appending the closed interval to paused_intervals with its reason so elapsed business minutes stay honest about what was parked and why. Only a paused clock can resume; anything else is 409 INVALID_CLOCK_TRANSITION. Returns 200 with the updated clock. Required: tenant_id.",
        "operationId": "post_api_sla_clocks_clock_id_resume",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "resume_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "INVALID_CLOCK_TRANSITION — clock <id> cannot move running -> running"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/{clock_id}/satisfy": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Satisfy with an accepted evidence kind, a reference and a named actor",
        "description": "Close the promise — but only on the evidence the policy asked for. Every unmet requirement of the satisfaction_contract is collected before refusing, so one 422 SATISFACTION_EVIDENCE_INSUFFICIENT carries a `missing` array naming ALL of them rather than making the caller discover them one rejection at a time. A refused satisfy leaves the clock exactly as it was: no half-closed promise. An evidence kind outside accepted_kinds is refused by name. Without this contract \"satisfied\" would mean whatever the closing service felt like asserting and the attainment number would stop measuring anything. A BREACHED clock can still be satisfied, and that is the common case rather than an edge one: the deadline passes and somebody answers anyway. Refusing it would leave the clock breached forever with no satisfied_at, so nothing could record when the late response happened and a report could not tell a miss that was eventually answered from one that was abandoned. Nothing is laundered by allowing it — satisfied_at is after due_at so attainment still counts the clock as a miss, the breach record and its cause are immutable rows of their own, and the event carries within_target: false. A satisfied or cancelled clock cannot be satisfied again (409). Returns 200 with the closed clock. Required: tenant_id plus whatever the policy contract demands.",
        "operationId": "post_api_sla_clocks_clock_id_satisfy",
        "parameters": [
          {
            "name": "clock_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "clock_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "satisfy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "evidence_ref": "message:{{dynamic:uuid}}",
                    "evidence_kind": "outbound_reply",
                    "evidence_count": 1,
                    "satisfied_by": "persona:backup",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "409": {
            "description": "INVALID_CLOCK_TRANSITION — clock <id> cannot move satisfied -> satisfied"
          },
          "422": {
            "description": "SATISFACTION_EVIDENCE_INSUFFICIENT — clock <id> cannot be satisfied: an evidence reference is required by this policy"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "evidence_kind": {
                    "type": "string"
                  },
                  "evidence_count": {
                    "type": "integer"
                  },
                  "satisfied_by": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "evidence_ref": "message:{{dynamic:uuid}}",
                "evidence_kind": "outbound_reply",
                "evidence_count": 1,
                "satisfied_by": "persona:backup",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "List the running clocks for this tenant",
        "description": "List clocks by deadline, narrowed by subject_ref, policy_id, state or owner_ref. Ordered by due_at ascending so the next thing to miss is first. Returns 200 with clocks and a count. Requires tenant_id.",
        "operationId": "get_api_sla_clocks",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "clock_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "A signal that arrived ten days ago — long past its two-hour promise",
        "description": "Start one promise about one subject. source_timestamp is WHEN THE SIGNAL HAPPENED — the message arrived, the form was submitted — not when the platform got around to noticing, and due_at is computed from it in business minutes on the policy calendar, so an overnight arrival is due after the promised amount of OPEN time rather than instantly breaching at opening. source_timestamp, started_at and due_at are then immutable, enforced by a database trigger: merge, reassignment and backup takeover move ownership and never the clock, because restarting it would erase the wait the person on the other end has already had. If a live clock already exists for this policy and subject the existing one is returned with created:false and 200 rather than a second clock, which would double-count the promise and fire the ladder twice. Returns 201 on a genuine start. Required: tenant_id, policy_id, subject_ref.",
        "operationId": "post_api_sla_clocks",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "clock_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "request:{{dynamic:uuid}}",
                    "source_timestamp": "2026-01-15T10:30:00Z",
                    "owner_ref": "persona:{{dynamic:uuid}}",
                    "metadata": {
                      "source_ref": "web_form",
                      "priority": "high"
                    },
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, policy_id and subject_ref are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_POLICY_NOT_FOUND — policy <id> not found for tenant"
          },
          "422": {
            "description": "CALENDAR_NEVER_OPEN — calendar <id> is never open"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "policy_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "source_timestamp": {
                    "type": "string"
                  },
                  "owner_ref": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "source_ref": {
                        "type": "string"
                      },
                      "priority": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "policy_id": "<id from sla>",
                "subject_ref": "request:{{dynamic:uuid}}",
                "source_timestamp": "<generated pastdatetime-10d>",
                "owner_ref": "persona:{{dynamic:uuid}}",
                "metadata": {
                  "source_ref": "web_form",
                  "priority": "high"
                },
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/clocks/merge": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Merge the duplicate request into the original",
        "description": "When two subjects turn out to be one, fold the absorbed clock into the survivor. The survivor keeps ITS OWN timing — the promise the platform made first is the one it owes — and records merged_from_ref so it still names what it absorbed; the absorbed clock is cancelled with a pointer to the survivor so the trail stays intact. Neither clock's source_timestamp, started_at or due_at moves, and a database trigger enforces that: a merge is one of the moments a system is most tempted to \"refresh\" a clock, which would erase a wait somebody has already had. Returns 200 with both clocks. MANUAL: needs TWO independently created clocks and the create->capture chain yields one clock_id, so an automated case would have to merge a clock into itself. Covered by packages/sdk-sla/tests/clock.integration.test.ts. Required: tenant_id, surviving_clock_id, merged_clock_id.",
        "operationId": "post_api_sla_clocks_merge",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "merge_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "surviving_clock_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "merged_clock_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, surviving_clock_id and merged_clock_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_CLOCK_NOT_FOUND — clock <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "surviving_clock_id": {
                    "type": "string"
                  },
                  "merged_clock_id": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "surviving_clock_id": "<id from sla>",
                "merged_clock_id": "<id from sla>",
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/sla/policies": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "List active request policies",
        "description": "List the tenant's policies in slug order, optionally narrowed by subject_kind and is_active. Returns 200 with policies and a count. Requires tenant_id.",
        "operationId": "get_api_sla_policies",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "A two-hour response promise with one legal pause and a real satisfaction contract",
        "description": "Create the promise itself: which subject_kind it covers, for how many BUSINESS minutes, on which calendar, which subjects qualify (qualifying_predicate, evaluated as data so a vertical narrows a promise without a platform change), what may pause the clock (pause_conditions, each with a max_minutes cap so a clock cannot be parked indefinitely to dodge a breach) and what actually closes it (satisfaction_contract — without it \"satisfied\" degrades into whatever the closing service felt like asserting). Returns 201. Required: tenant_id, slug, name, subject_kind, duration_minutes (> 0) and calendar_id.",
        "operationId": "post_api_sla_policies",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "first-response-{{dynamic:uuid}}",
                    "name": "First response",
                    "description": "Respond within two business hours",
                    "subject_kind": "request",
                    "duration_minutes": 120,
                    "calendar_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "qualifying_predicate": {
                      "all": [
                        {
                          "field": "priority",
                          "op": "in",
                          "value": [
                            "high",
                            "urgent"
                          ]
                        }
                      ]
                    },
                    "pause_conditions": [
                      {
                        "reason": "awaiting_subject_reply",
                        "max_minutes": 4320
                      }
                    ],
                    "satisfaction_contract": {
                      "requires_evidence_ref": true,
                      "accepted_kinds": [
                        "outbound_reply",
                        "resolution"
                      ],
                      "min_evidence_count": 1,
                      "requires_actor": true
                    },
                    "metadata": {
                      "owner": "qa-runner"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, slug, name, subject_kind, duration_minutes and calendar_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "subject_kind": {
                    "type": "string"
                  },
                  "duration_minutes": {
                    "type": "integer"
                  },
                  "calendar_id": {
                    "type": "string"
                  },
                  "qualifying_predicate": {
                    "type": "object",
                    "properties": {
                      "all": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "op": {
                              "type": "string"
                            },
                            "value": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    }
                  },
                  "pause_conditions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "reason": {
                          "type": "string"
                        },
                        "max_minutes": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "satisfaction_contract": {
                    "type": "object",
                    "properties": {
                      "requires_evidence_ref": {
                        "type": "boolean"
                      },
                      "accepted_kinds": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "min_evidence_count": {
                        "type": "integer"
                      },
                      "requires_actor": {
                        "type": "boolean"
                      }
                    }
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "owner": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "slug": "first-response-{{dynamic:uuid}}",
                "name": "First response",
                "description": "Respond within two business hours",
                "subject_kind": "request",
                "duration_minutes": 120,
                "calendar_id": "<id from sla>",
                "qualifying_predicate": {
                  "all": [
                    {
                      "field": "priority",
                      "op": "in",
                      "value": [
                        "high",
                        "urgent"
                      ]
                    }
                  ]
                },
                "pause_conditions": [
                  {
                    "reason": "awaiting_subject_reply",
                    "max_minutes": 4320
                  }
                ],
                "satisfaction_contract": {
                  "requires_evidence_ref": true,
                  "accepted_kinds": [
                    "outbound_reply",
                    "resolution"
                  ],
                  "min_evidence_count": 1,
                  "requires_actor": true
                },
                "metadata": {
                  "owner": "qa-runner"
                }
              }
            }
          }
        }
      }
    },
    "/api/sla/policies/{policy_id}": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the policy created above",
        "description": "Read one policy with its qualifying predicate, pause conditions and satisfaction contract — the three pieces of data a caller needs to know why a clock qualified, what may pause it and what will close it. Returns 200, or 404 SLA_POLICY_NOT_FOUND outside the tenant.",
        "operationId": "get_api_sla_policies_policy_id",
        "parameters": [
          {
            "name": "policy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "policy_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_POLICY_NOT_FOUND — policy <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/sla/policies/{policy_id}/rungs": {
      "get": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Read the ladder including retired rungs",
        "description": "The policy's ladder in rung order — active rungs by default, all of them with include_inactive=true. Returns 200 with rungs and a count. Each rung carries its offset, audience, severity, action and remediation hint, which is the whole configuration: nothing about escalation lives in code.",
        "operationId": "get_api_sla_policies_policy_id_rungs",
        "parameters": [
          {
            "name": "policy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "policy_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "rung_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Warn the owner thirty business minutes before the deadline",
        "description": "Add a rung to the policy ladder. The ladder is DATA: when it fires, who hears about it, how loudly, what action runs and what the recipient should do. The stored anchor is business minutes from the clock's start — the same arithmetic that produced due_at, so a rung and a deadline can never disagree about the calendar — but minutes_before_due and minutes_after_due are accepted and normalised once at insert, so the row that fires is the row an operator can read. Give exactly ONE of the three offsets; giving two or none returns 422 INVALID_RUNG_OFFSET, as does a minutes_before_due longer than the promise itself. The audience is resolved AT FIRE TIME, not now, because the person on call at 02:00 is not the person who was on call when the policy was written. Returns 201.",
        "operationId": "post_api_sla_policies_policy_id_rungs",
        "parameters": [
          {
            "name": "policy_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "policy_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "rung_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "rung_index": 0,
                    "label": "pre-breach warning",
                    "minutes_before_due": 30,
                    "action": "notify",
                    "severity": "warning",
                    "audience": {
                      "kind": "owner"
                    },
                    "action_config": {
                      "channel": "email"
                    },
                    "remediation_hint": "answer the request or hand it to the backup",
                    "metadata": {
                      "owner": "qa-runner"
                    },
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, rung_index and action are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_POLICY_NOT_FOUND — policy <id> not found for tenant"
          },
          "422": {
            "description": "INVALID_RUNG_OFFSET — give exactly one of offset_minutes, minutes_before_due or minutes_after_due"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "rung_index": {
                    "type": "integer"
                  },
                  "label": {
                    "type": "string"
                  },
                  "minutes_before_due": {
                    "type": "integer"
                  },
                  "action": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string"
                  },
                  "audience": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      }
                    }
                  },
                  "action_config": {
                    "type": "object",
                    "properties": {
                      "channel": {
                        "type": "string"
                      }
                    }
                  },
                  "remediation_hint": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "owner": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "rung_index": 0,
                "label": "pre-breach warning",
                "minutes_before_due": 30,
                "action": "notify",
                "severity": "warning",
                "audience": {
                  "kind": "owner"
                },
                "action_config": {
                  "channel": "email"
                },
                "remediation_hint": "answer the request or hand it to the backup",
                "metadata": {
                  "owner": "qa-runner"
                }
              }
            }
          }
        }
      }
    },
    "/api/sla/rungs/{rung_id}": {
      "patch": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Restore the rung (idempotent when it is already active)",
        "description": "Activate or retire one rung without deleting its firing history — the ledger rows that reference it stay queryable, which is why the FK is ON DELETE RESTRICT and there is no DELETE on this resource. A retired rung is simply never claimed by the tick again. Returns 200 with the updated rung; 404 SLA_LADDER_RUNG_NOT_FOUND outside the tenant.",
        "operationId": "patch_api_sla_rungs_rung_id",
        "parameters": [
          {
            "name": "rung_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "rung_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "rung_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "is_active": true,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and is_active (boolean) are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SLA_LADDER_RUNG_NOT_FOUND — ladder rung <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "is_active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "is_active": true
              }
            }
          }
        }
      }
    },
    "/api/sla/systemic-incidents/open-pending": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Drain the pending queue (zero when every group already opened)",
        "description": "Retry the systemic groups whose incident never opened, because no incident opener was wired yet or the last attempt failed. The breach records themselves were never at risk — the incident is opened OUTSIDE the record's write precisely so that a provider being down cannot cost the record — so wiring the integration late or recovering from an outage costs a delay and nothing else. Returns 200 with attempted and opened counts. Required: tenant_id.",
        "operationId": "post_api_sla_systemic_incidents_open_pending",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "open_pending_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "limit": 25,
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "limit": {
                    "type": "integer"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "limit": 25
              }
            }
          }
        }
      }
    },
    "/api/sla/tick": {
      "post": {
        "tags": [
          "sdk-sla"
        ],
        "summary": "Tick the tenant — the overdue clock has a rung waiting",
        "description": "Evaluate every running clock against its ladder and fire whatever has come due. Safe to call as often as you like from as many callers as you like: each rung is claimed by inserting a ledger row under a UNIQUE (clock_id, rung_id), so of N concurrent ticks exactly one fires a given rung and the rest report it as skipped_duplicate. No lock is held across the action, because an escalation that pages somebody twice is worse than one that pages them a second late. Also safe to call LATE — a rung that came due an hour ago fires now, and both fire_at and fired_at are recorded so the report shows the ladder ran behind rather than pretending it did not. Failed and stale-claimed firings are retried in the same pass with exponential backoff, never re-firing anything already fired. A rung whose action has no registered handler FAILS visibly instead of being recorded as a silent success. Returns 200 with the counters. Required: tenant_id.",
        "operationId": "post_api_sla_tick",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "tick_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "limit": 100,
                    "actor_id": "qa-runner",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "limit": {
                    "type": "integer"
                  },
                  "actor_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "limit": 100,
                "actor_id": "qa-runner"
              }
            }
          }
        }
      }
    },
    "/api/source-assertions/{assertion_id}/supersede": {
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Supersede the licensed claim with a corrected one",
        "description": "Replace a claim by inserting the successor and STAMPING the prior row (status=SUPERSEDED, superseded_by, superseded_at) in one transaction. The prior value, dates, origin and confidence are never touched — the database trigger rejects any other UPDATE and every DELETE — so both claims stay queryable with their original provenance. Supersede is one-way: a second attempt on an already-superseded claim returns 409, and a concurrent supersede rolls the transaction back rather than leaving an orphan successor. Any replacement field omitted is inherited from the prior claim (subject_ref, attribute, capture_id, origin_class). Returns 200: the response carries both rows. Required: tenant_id and the replacement value.",
        "operationId": "post_api_source_assertions_assertion_id_supersede",
        "parameters": [
          {
            "name": "assertion_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "assertion_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "supersede_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "value": "qa.user@example.com",
                    "origin_class": "LICENSED_THIRD_PARTY",
                    "subject_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "attribute": "email",
                    "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "confidence": 0.81,
                    "effective_from": "2026-01-15T10:30:00Z",
                    "effective_to": "2026-01-15T10:30:00Z",
                    "retrieved_at": "2026-01-15T10:30:00Z",
                    "status": "ASSERTION",
                    "evidence_ref": "evidence:{{dynamic:uuid}}",
                    "is_pii": true,
                    "metadata": {
                      "correction": true
                    },
                    "reason": "the broker issued a correction",
                    "actor_id": "qa-runner",
                    "purpose": "claim correction",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and the replacement value are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "ASSERTION_NOT_FOUND — assertion <id> not found for tenant"
          },
          "409": {
            "description": "ASSERTION_ALREADY_SUPERSEDED — assertion <id> was already superseded by <id>"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  },
                  "origin_class": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "attribute": {
                    "type": "string"
                  },
                  "capture_id": {
                    "type": "string"
                  },
                  "confidence": {
                    "type": "number"
                  },
                  "effective_from": {
                    "type": "string"
                  },
                  "effective_to": {
                    "type": "string"
                  },
                  "retrieved_at": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "is_pii": {
                    "type": "boolean"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "correction": {
                        "type": "boolean"
                      }
                    }
                  },
                  "reason": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "value": "<generated email>",
                "origin_class": "LICENSED_THIRD_PARTY",
                "subject_ref": "<id from source-record>",
                "attribute": "email",
                "capture_id": "<id from source-record>",
                "confidence": 0.81,
                "effective_from": "<generated pastdatetime-1d>",
                "effective_to": "<generated futuredatetime+90d>",
                "retrieved_at": "<generated pastdatetime-10m>",
                "status": "ASSERTION",
                "evidence_ref": "evidence:{{dynamic:uuid}}",
                "is_pii": true,
                "metadata": {
                  "correction": true
                },
                "reason": "the broker issued a correction",
                "actor_id": "qa-runner",
                "purpose": "claim correction",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-assertions": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "List every coexisting claim for the subject and attribute",
        "description": "Return EVERY claim matching the filter, superseded rows INCLUDED by default. Hiding them would recreate exactly the last-write-wins behaviour this SDK exists to prevent; a caller that wants one value asks sdk-projection. Ordering is a hint, not a decision: PRIMARY then SURVIVES then ASSERTION then SUPERSEDED, tie-broken by origin trust, then confidence, then recency. effective_at takes the bitemporal slice — only claims whose effective period contains that instant. exclude_superseded=true is the opt-in narrow view. Values stay enveloped in the list; revealing an identifier is a separate, narrower operation. limit is clamped to 1..500 (default 100). Required: tenant_id query param.",
        "operationId": "get_api_source_assertions",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "source_assertion_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Write a licensed third-party email claim",
        "description": "Write one claim: per this origin, over this effective period, the subject's <attribute> was <value>, retrieved at <retrieved_at>. The two time axes are deliberately separate — effective_from/effective_to say when the fact held in the world, retrieved_at says when we learned it — so a late-arriving source can correct history without pretending we knew earlier. Conflicting claims for the same subject+attribute from different origins are the NORMAL case and coexist; nothing here resolves a display value (that is sdk-projection). A value whose attribute is a direct identifier (email, phone, address, national_id, names, date_of_birth...) is envelope-encrypted BEFORE the insert, so the plaintext never reaches the column, the query log or a replica — the response returns value_encrypted:true and a vault_key_ref. Once written, the claim is immutable: correcting it means superseding it. Required: tenant_id, subject_ref, attribute, value, origin_class.",
        "operationId": "post_api_source_assertions",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "source_assertion_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "subject_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "attribute": "email",
                    "value": "qa.user@example.com",
                    "origin_class": "LICENSED_THIRD_PARTY",
                    "confidence": 0.62,
                    "effective_from": "2026-01-15T10:30:00Z",
                    "effective_to": "2026-01-15T10:30:00Z",
                    "retrieved_at": "2026-01-15T10:30:00Z",
                    "status": "ASSERTION",
                    "evidence_ref": "evidence:{{dynamic:uuid}}",
                    "is_pii": true,
                    "metadata": {
                      "batch": "sample-slug"
                    },
                    "actor_id": "qa-runner",
                    "purpose": "claim capture",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, subject_ref, attribute, value and origin_class are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "capture_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "attribute": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string"
                  },
                  "origin_class": {
                    "type": "string"
                  },
                  "confidence": {
                    "type": "number"
                  },
                  "effective_from": {
                    "type": "string"
                  },
                  "effective_to": {
                    "type": "string"
                  },
                  "retrieved_at": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "is_pii": {
                    "type": "boolean"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "batch": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "capture_id": "<id from source-record>",
                "subject_ref": "<id from source-record>",
                "attribute": "email",
                "value": "<generated email>",
                "origin_class": "LICENSED_THIRD_PARTY",
                "confidence": 0.62,
                "effective_from": "<generated pastdatetime-30d>",
                "effective_to": "<generated futuredatetime+30d>",
                "retrieved_at": "<generated pastdatetime-1h>",
                "status": "ASSERTION",
                "evidence_ref": "evidence:{{dynamic:uuid}}",
                "is_pii": true,
                "metadata": {
                  "batch": "<generated slug>"
                },
                "actor_id": "qa-runner",
                "purpose": "claim capture",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-records/{capture_id}/crosswalks": {
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Link the capture to its external system identifier",
        "description": "Record the external system + id this capture came from, kept forever. The pair is immutable and the row is undeletable by database trigger: overwriting a crosswalk would silently break the link back to the source system. Idempotent per (tenant, external_system, external_id) — re-linking the same external identity returns the EXISTING crosswalk rather than erroring, which is what makes a retried import safe. Required: tenant_id, external_system, external_id.",
        "operationId": "post_api_source_records_capture_id_crosswalks",
        "parameters": [
          {
            "name": "capture_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "capture_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "crosswalk_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "external_system": "registry-{{dynamic:slug}}",
                    "external_id": "EXT-{{dynamic:uuid}}",
                    "subject_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "metadata": {
                      "linked_by": "qa-runner"
                    },
                    "actor_id": "qa-runner",
                    "purpose": "crosswalk linkage",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, external_system and external_id are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SOURCE_RECORD_NOT_FOUND — capture <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "external_system": {
                    "type": "string"
                  },
                  "external_id": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "linked_by": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "external_system": "registry-{{dynamic:slug}}",
                "external_id": "EXT-{{dynamic:uuid}}",
                "subject_ref": "<id from source-record>",
                "metadata": {
                  "linked_by": "qa-runner"
                },
                "actor_id": "qa-runner",
                "purpose": "crosswalk linkage",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-records/{capture_id}": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Read the capture created by the producer",
        "description": "Read a single capture and every external identifier crosswalked to it. The crosswalks are returned inline because an external id is what makes the capture re-findable in the system it came from — reading one without the other is almost always a bug. 404 when the capture does not belong to the tenant, so a capture id from another tenant is indistinguishable from one that does not exist. Required: tenant_id query param.",
        "operationId": "get_api_source_records_capture_id",
        "parameters": [
          {
            "name": "capture_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "capture_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "source_record_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SOURCE_RECORD_NOT_FOUND — capture <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/source-records/{capture_id}/normalize": {
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Normalize the capture to P1_NORMALIZED",
        "description": "Extract structured fields from the capture and move it P0_CAPTURED -> P1_NORMALIZED. Extraction is delegated to sdk-parsing through an injected hook; with no parser wired the default is a structural pass-through, so the rung still advances honestly rather than failing closed. The result lands in `normalized` ALONGSIDE raw_evidence, which the database refuses to let anyone overwrite. Idempotent: re-normalizing an already-normalized capture refreshes `normalized` and returns 200 without appending a second transition to the audit chain, because the trust state did not move. Refused with 409 for a quarantined capture — a record with no established provenance does not climb. Returns 200, not 201: it moves an existing resource. Required: tenant_id.",
        "operationId": "post_api_source_records_capture_id_normalize",
        "parameters": [
          {
            "name": "capture_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "capture_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "normalize_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "actor_id": "qa-runner",
                    "purpose": "normalization",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SOURCE_RECORD_NOT_FOUND — capture <id> not found for tenant"
          },
          "409": {
            "description": "RECORD_QUARANTINED — capture <id> is quarantined and cannot be promoted"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "actor_id": "qa-runner",
                "purpose": "normalization",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-records/{capture_id}/promote": {
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Promote the normalized capture to P2_CANDIDATE",
        "description": "Promote a capture exactly ONE rung, if that rung's evidence requirement is met. Skipping a rung or moving backwards is refused with 409 INVALID_TRUST_TRANSITION listing what IS allowed. Per-rung requirements: P2_CANDIDATE needs a normalized payload; P3_LINKED needs a subject_ref; P4_DIRECT needs an evidence_ref whose evidence_origin_class is FIRST-PARTY (USER_PROVIDED, FIRST_PARTY_DIRECT, TENANT_FIRST_PARTY_CRM or USER_AUTHORIZED_CONTACT_STORE). A LICENSED_THIRD_PARTY or PUBLIC_RECORD evidence reference does NOT satisfy P4 and is refused with a typed 422 naming the missing first-party evidence — P4 asserts the subject themselves told us, so bought data can never reach it. Every refusal leaves the record where it was and appends NO audit entry claiming promotion. Concurrent double-promotion is a no-op for the loser (the update is guarded on the current state). Returns 200: it moves an existing resource. Required: tenant_id, to_state.",
        "operationId": "post_api_source_records_capture_id_promote",
        "parameters": [
          {
            "name": "capture_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "capture_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "promote_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "to_state": "P2_CANDIDATE",
                    "subject_ref": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "evidence_ref": "evidence:{{dynamic:uuid}}",
                    "evidence_origin_class": "FIRST_PARTY_DIRECT",
                    "actor_id": "qa-runner",
                    "purpose": "trust promotion",
                    "decision_ref": "decision:{{dynamic:uuid}}",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and to_state are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "SOURCE_RECORD_NOT_FOUND — capture <id> not found for tenant"
          },
          "409": {
            "description": "RECORD_QUARANTINED — capture <id> is quarantined and cannot be promoted"
          },
          "422": {
            "description": "NORMALIZATION_REQUIRED — cannot promote <id> P1_NORMALIZED -> P2_CANDIDATE: missing normalized payload"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "to_state": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "evidence_origin_class": {
                    "type": "string"
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "decision_ref": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "to_state": "P2_CANDIDATE",
                "subject_ref": "<id from source-record>",
                "evidence_ref": "evidence:{{dynamic:uuid}}",
                "evidence_origin_class": "FIRST_PARTY_DIRECT",
                "actor_id": "qa-runner",
                "purpose": "trust promotion",
                "decision_ref": "decision:{{dynamic:uuid}}",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-records": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "List captures for the tenant",
        "description": "List a tenant's captures, newest first. Every filter is optional and ANDed: trust_state and origin_class narrow by the ENUM columns, source_system and subject_ref by exact match. limit is clamped to 1..500 (default 50) so an unbounded page can never be requested, and offset is floored at 0. Returns an empty array — not a 404 — when nothing matches. Required: tenant_id query param.",
        "operationId": "get_api_source_records",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "source_record_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Capture a public-record payload at P0_CAPTURED",
        "description": "Land one retrieval from one source system as an immutable capture at trust state P0_CAPTURED. raw_evidence is stored exactly as received and is frozen by a database trigger, as is origin_class. Idempotent on the content fingerprint: a repeat of the same payload for the same tenant returns the ORIGINAL capture with created=false instead of forking a second lineage (the fingerprint is derived from the payload with object keys sorted, so a re-serialized body still dedupes). Edge case that matters: an absent or unrecognised origin_class does NOT default to anything plausible — the record lands UNKNOWN_QUARANTINED with quarantine_reason set and quarantined=true, and a quarantined record is refused every later promotion. Required: tenant_id, source_system, raw_evidence.",
        "operationId": "post_api_source_records",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "source_record_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "source_system": "registry-{{dynamic:slug}}",
                    "raw_evidence": {
                      "record_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "email": "qa.user@example.com",
                      "display_name": "Acme QA Sample"
                    },
                    "fingerprint": "fp-{{dynamic:uuid}}",
                    "source_external_id": "ext-{{dynamic:slug}}",
                    "origin_class": "PUBLIC_RECORD",
                    "evidence_kind": "API_RESPONSE",
                    "evidence_ref": "evidence:{{dynamic:uuid}}",
                    "subject_ref": "subject:{{dynamic:uuid}}",
                    "retrieved_at": "2026-01-15T10:30:00Z",
                    "metadata": {
                      "ingest_batch": "sample-slug"
                    },
                    "actor_id": "qa-runner",
                    "purpose": "provenance capture",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, source_system and raw_evidence are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "source_system": {
                    "type": "string"
                  },
                  "raw_evidence": {
                    "type": "object",
                    "properties": {
                      "record_id": {
                        "type": "string"
                      },
                      "email": {
                        "type": "string"
                      },
                      "display_name": {
                        "type": "string"
                      }
                    }
                  },
                  "fingerprint": {
                    "type": "string"
                  },
                  "source_external_id": {
                    "type": "string"
                  },
                  "origin_class": {
                    "type": "string"
                  },
                  "evidence_kind": {
                    "type": "string"
                  },
                  "evidence_ref": {
                    "type": "string"
                  },
                  "subject_ref": {
                    "type": "string"
                  },
                  "retrieved_at": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "ingest_batch": {
                        "type": "string"
                      }
                    }
                  },
                  "actor_id": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "source_system": "registry-{{dynamic:slug}}",
                "raw_evidence": {
                  "record_id": "<generated uuid>",
                  "email": "<generated email>",
                  "display_name": "<generated name>"
                },
                "fingerprint": "fp-{{dynamic:uuid}}",
                "source_external_id": "ext-{{dynamic:slug}}",
                "origin_class": "PUBLIC_RECORD",
                "evidence_kind": "API_RESPONSE",
                "evidence_ref": "evidence:{{dynamic:uuid}}",
                "subject_ref": "subject:{{dynamic:uuid}}",
                "retrieved_at": "<generated pastdatetime-1h>",
                "metadata": {
                  "ingest_batch": "<generated slug>"
                },
                "actor_id": "qa-runner",
                "purpose": "provenance capture",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-rights/attestations/{attestation_id}": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Read the attestation just signed",
        "description": "Read one signed attestation with its full terms: attestor principal, permitted uses, jurisdiction, licence reference, collection period, evidence blob reference and the signature over them. 404 when the attestation belongs to another tenant, so an id from elsewhere is indistinguishable from one that does not exist. Required: tenant_id query param.",
        "operationId": "get_api_source_rights_attestations_attestation_id",
        "parameters": [
          {
            "name": "attestation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "attestation_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "attestation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "404": {
            "description": "ATTESTATION_NOT_FOUND — attestation <id> not found for tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/source-rights/attestations": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "List attestations for the capture",
        "description": "List a tenant's signed attestations, newest first, optionally narrowed to one capture or one origin class. Returns an empty array rather than a 404 when nothing matches. limit is clamped to 1..500 (default 50). Required: tenant_id query param.",
        "operationId": "get_api_source_rights_attestations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "attestation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      },
      "post": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Sign a licensed-source attestation with its licence evidence",
        "description": "Record and sign the answer to \"were we allowed to hold this, and what may we do with it?\". The signature covers the TERMS, not just the row: source fingerprint, mapping version and the normalized permitted-use set, so a later claim of broader permissions cannot pass as the original grant. permitted_uses is trimmed, lower-cased, deduped and sorted before signing, so the same grant always signs identically. HARD RULE: an origin_class of LICENSED_THIRD_PARTY or PARTNER_PROVIDED is REFUSED with 422 unless an evidence blob reference is present — bought or partner-supplied data must carry its paperwork, and the refusal happens before anything is written. evidence_payload is captured through sdk-evidence when that bridge is wired; with no bridge it does NOT satisfy the rule, because a fabricated blob id is exactly the failure this prevents. The row is immutable once signed. Either capture_id or source_fingerprint must be present to bind the signature to. Required: tenant_id, attestor_principal, origin_class, permitted_uses[].",
        "operationId": "post_api_source_rights_attestations",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": {
                    "attestation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "tenant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "attestor_principal": "principal:compliance-officer",
                    "origin_class": "LICENSED_THIRD_PARTY",
                    "permitted_uses": [
                      "outreach",
                      "analytics"
                    ],
                    "capture_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "source_fingerprint": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "jurisdiction": "EU",
                    "license_ref": "LIC-{{dynamic:slug}}",
                    "collection_period_start": "2026-01-15T10:30:00Z",
                    "collection_period_end": "2026-01-15T10:30:00Z",
                    "evidence_blob_ref": "evidence:{{dynamic:uuid}}",
                    "evidence_kind": "LICENSE_TERMS",
                    "mapping_version": "map-{{dynamic:slug}}",
                    "metadata": {
                      "reviewed_by": "qa-runner"
                    },
                    "purpose": "rights attestation",
                    "causation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "status": "active",
                    "created_at": "2026-01-15T10:30:00Z",
                    "updated_at": "2026-01-15T10:30:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id, attestor_principal, origin_class and permitted_uses[] are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          },
          "422": {
            "description": "ATTESTATION_EVIDENCE_REQUIRED — origin_class LICENSED_THIRD_PARTY requires an evidence blob reference before it can be attested"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "attestor_principal": {
                    "type": "string"
                  },
                  "origin_class": {
                    "type": "string"
                  },
                  "permitted_uses": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "capture_id": {
                    "type": "string"
                  },
                  "source_fingerprint": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  },
                  "license_ref": {
                    "type": "string"
                  },
                  "collection_period_start": {
                    "type": "string"
                  },
                  "collection_period_end": {
                    "type": "string"
                  },
                  "evidence_blob_ref": {
                    "type": "string"
                  },
                  "evidence_kind": {
                    "type": "string"
                  },
                  "mapping_version": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object",
                    "properties": {
                      "reviewed_by": {
                        "type": "string"
                      }
                    }
                  },
                  "purpose": {
                    "type": "string"
                  },
                  "causation_id": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "attestor_principal": "principal:compliance-officer",
                "origin_class": "LICENSED_THIRD_PARTY",
                "permitted_uses": [
                  "outreach",
                  "analytics"
                ],
                "capture_id": "<id from source-record>",
                "source_fingerprint": "<id from source-record>",
                "jurisdiction": "EU",
                "license_ref": "LIC-{{dynamic:slug}}",
                "collection_period_start": "<generated pastdatetime-30d>",
                "collection_period_end": "<generated futuredatetime+365d>",
                "evidence_blob_ref": "evidence:{{dynamic:uuid}}",
                "evidence_kind": "LICENSE_TERMS",
                "mapping_version": "map-{{dynamic:slug}}",
                "metadata": {
                  "reviewed_by": "qa-runner"
                },
                "purpose": "rights attestation",
                "causation_id": "<generated uuid>"
              }
            }
          }
        }
      }
    },
    "/api/source-rights/permitted-use": {
      "get": {
        "tags": [
          "sdk-source-record"
        ],
        "summary": "Check an attested purpose against the signed rights",
        "description": "The refusal mechanism, over HTTP: a consumer asks whether the source rights cover a purpose BEFORE acting, which is what keeps licensed data out of an incompatible use. FAILS CLOSED at every step — no attestation (NO_ATTESTATION), a purpose outside the attested set (PURPOSE_NOT_ATTESTED), or a lapsed collection period (COLLECTION_PERIOD_LAPSED) all return permitted:false with the reason named and the uses that ARE granted, so the caller sees its next best option. Absence of evidence is never read as permission. Scope the question by subject_ref (checks every attestation covering captures linked to that subject; ANY grant suffices, since each covers its own source), or by capture_id / source_fingerprint for one source. `at` defaults to now. ALWAYS 200 — \"not permitted\" is a successful answer to the question, not a failed request. Required: tenant_id, purpose.",
        "operationId": "get_api_source_rights_permitted_use",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "example": {
                  "success": true,
                  "data": [
                    {
                      "permitted_use_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "status": "active"
                    }
                  ],
                  "total": 1
                }
              }
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id and purpose query params are required"
          },
          "401": {
            "description": "Unauthorized — missing or invalid token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assignment/simulations": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "The tenant's runs come back newest first",
        "description": "The list a reviewer opens before they know which simulation_id they want: this tenant's recorded runs, newest first, each row a summary (simulation_id, candidate_version, rule_set_name, subjects_replayed, created_at) rather than the whole report - a full report per row would make the common case, browsing, pay for the rare case, reading one. Filters narrow to a rule set by name and to one candidate_version, which is how the question is actually asked: show me every simulation of the version we shipped. QA edge cases: results are tenant-scoped, so another tenant's runs never appear regardless of filter; a tenant with no runs gets 200 and an empty array, not 404, because no simulations is a valid state and not a missing resource; limit is clamped to a 1-500 range so a caller cannot ask for the whole table; rule_set_name and candidate_version are independent and combine; an unknown rule_set_name yields an empty array rather than an error, because a name that matched nothing is an answer.",
        "operationId": "get_api_assignment_simulations",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — candidate_version must be an integer"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/assignment/simulations/{simulation_id}": {
      "get": {
        "tags": [
          "sdk-assignment"
        ],
        "summary": "A cited simulation resolves to the report it produced",
        "description": "Resolves the simulation_id a routing proposal cited back to the IDENTICAL report the simulate call returned. This is the half that makes a simulation evidence rather than a screenshot: a routing change is proposed on the strength of a simulation, approved weeks later and questioned months after that, and candidate_version identifies the RULES, never the RUN - two simulations of the same version over different windows or candidate pools are different evidence carrying the same version number. The stored row is immutable, so the numbers a reviewer re-opens are the numbers the decision was made on. QA edge cases: the run is tenant-scoped, so a simulation_id belonging to another tenant is 404 and NOT another tenant's evidence - absence and forbidden are deliberately indistinguishable here, because confirming an id exists elsewhere leaks that a simulation happened; a well-formed but never-issued id is 404; a malformed id is refused as a validation error before it reaches the database; the returned body carries created_at alongside the report so a reviewer can see WHEN the question was asked, and its side_effects block still reads all zeros because recording a run is not a routing effect.",
        "operationId": "get_api_assignment_simulations_simulation_id",
        "parameters": [
          {
            "name": "simulation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "simulation_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — simulation_id must be a UUID"
          },
          "401": {
            "description": "Unauthorized — missing or invalid tenant token"
          },
          "404": {
            "description": "SIMULATION_NOT_FOUND — no simulation run with that id for this tenant"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/secrets/rotate": {
      "post": {
        "tags": [
          "sdk-secrets"
        ],
        "summary": "Rotate previously registered SecretRef",
        "description": "Rotates the KMS key version backing a registered SecretRef and stamps rotated_at on the catalog row, returning the rotation result. The reference is passed as `ref` in the BODY. Requires a valid tenant JWT (requireAuth). WHY THE BODY FORM: secretRefCatalog validates every reference against /^secret:\\/\\/(app|pool|tenant)\\/(.+)$/, so a conformant ref ALWAYS contains '://' and at least one further '/' - three or more path segments. A Fastify ':ref' parameter matches exactly ONE segment, so the sibling route POST /api/secrets/:ref/rotate can never match a real ref and 404s on every conformant value; it is retained in the code only as a harmless legacy path. The body is where a slash is not a delimiter, so this is the form that actually works and the one QA must use. Edge cases: an absent or whitespace-only ref is a 400 ValidationError naming the expected shape; rotating an unregistered ref is a 404 NotFound, matched on the service message starting with \"Secret reference not registered\"; rotation is repeatable but not idempotent - each call mints a new key version and rewrites rotated_at, so callers must not retry blindly; a KMS-side failure is a 500 InternalError, indistinguishable in status from a DB failure.",
        "operationId": "post_api_secrets_rotate",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — ref is required in the body, e.g. { \"ref\": \"secret://tenant/my-key\" }"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — Secret reference not registered: <ref>"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ref": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "ref": "<id from secrets>"
              }
            }
          }
        }
      }
    },
    "/api/keys/{key_id}/rotate": {
      "post": {
        "tags": [
          "sdk-api-keys"
        ],
        "summary": "Rotate the key just issued",
        "description": "Rotates an API key: mints replacement material and returns 201 with the new record and its one-time plaintext. DEPRECATED ALIAS - /api/keys/* is retained for older integrations behind a deprecate preHandler and delegates to exactly the same rotateKeyHandler as the canonical /api/api-keys/:key_id/rotate. New callers should use the canonical path. Answers 201 rather than 200 because rotation CREATES new key material rather than editing the existing row; the plaintext is returned exactly once and is unrecoverable afterwards, so it must be captured from this response. Rotation is not idempotent - each call mints another key. A key_id that is already revoked, or that belongs to another tenant, is 404 'No rotatable key with that id' rather than 403, so the route never confirms another tenant's keys exist.",
        "operationId": "post_api_keys_key_id_rotate",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — This credential carries no tenant context"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — No rotatable key with that id"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              },
              "example": {}
            }
          }
        }
      }
    },
    "/api/offers/{offer_id}": {
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Fetch the offer just created",
        "description": "Fetches a single offer by id and returns 200 with data.offer. tenant_id is a REQUIRED QUERY PARAMETER, not a claim: getOffer scopes by (tenant_id, offer_id), so omitting it is 400 rather than defaulting to the caller's tenant. That also means the 404 is tenant-scoped - an offer_id that exists under a different tenant answers 404 NotFound, not 403, so this route never confirms the existence of another tenant's offer. Returns the offer envelope only; it does not include versions or the current live version - use GET /api/offers/:offer_id/versions and GET /api/offers/:offer_id/current for those.",
        "operationId": "get_api_offers_offer_id",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/offers/{offer_id}/versions/{version_id}": {
      "get": {
        "tags": [
          "sdk-offer-catalog"
        ],
        "summary": "Fetch the version just created",
        "description": "Fetches one offer version by id and returns 200 with data.version. tenant_id is a REQUIRED QUERY PARAMETER, so omitting it is 400. NOTE the resolution asymmetry: getOfferVersion is called with (tenant_id, version_id) ONLY - offer_id is in the path for URL shape but is NOT used to scope the lookup, so a version_id belonging to a different offer under the same tenant resolves 200 through a mismatched offer_id in the path. Do not treat this route as a check that the version belongs to the offer. A version_id under a different TENANT is correctly 404, and 404 is used rather than 403 so the route never confirms another tenant's data. Route ordering matters here: this pattern is registered AFTER /versions/:version_id/features, so 'features' is matched by that route rather than being read as a version_id.",
        "operationId": "get_api_offers_offer_id_versions_version_id",
        "parameters": [
          {
            "name": "offer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "offer_id path parameter"
          },
          {
            "name": "version_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "version_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "NotFound — NotFound"
          },
          "500": {
            "description": "InternalError — Fastify default error payload from the uncaught service throw"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/relationships/{relationship_id}/attest": {
      "post": {
        "tags": [
          "sdk-rebac"
        ],
        "summary": "Attest the relationship as CONFIRMED with an evidence ref",
        "description": "Records the trust state of a relationship's contextual role and returns 200 with data.role. trust_state is required and must be one of CONFIRMED, CANDIDATE or DOCUMENTED. THE EVIDENCE RULE IS THE POINT: CONFIRMED and DOCUMENTED both mean 'somebody checked', so attestContextualRole refuses either without at least one non-empty evidence_ref - a claim that a check happened must say what was checked. CANDIDATE is the only state that may carry no evidence, because it asserts nothing was verified. The two 400s are distinguishable by code: VALIDATION_ERROR for a trust_state outside the enum (rejected in the controller before any service call), EVIDENCE_REQUIRED for a valid state missing its evidence (raised by the service). Re-attesting MERGES evidence_refs with what is already stored rather than replacing them, deduplicated, so promoting a CANDIDATE to CONFIRMED can supply only the new reference. An unknown relationship_id is 404 RELATIONSHIP_NOT_FOUND.",
        "operationId": "post_api_relationships_relationship_id_attest",
        "parameters": [
          {
            "name": "relationship_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "relationship_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — trust_state must be one of CONFIRMED, CANDIDATE, DOCUMENTED"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "404": {
            "description": "RELATIONSHIP_NOT_FOUND — NotFound"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "trust_state": {
                    "type": "string"
                  },
                  "evidence_refs": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "example": {
                "trust_state": "CONFIRMED",
                "evidence_refs": [
                  "evidence://qa/attestation-{{dynamic:slug}}"
                ]
              }
            }
          }
        }
      }
    },
    "/api/vault/keys/{key_id}": {
      "get": {
        "tags": [
          "sdk-vault"
        ],
        "summary": "A root-tier key is NOT visible to a tenant token",
        "description": "Reads ONE vault key, and only if it belongs to the calling tenant. Returns metadata only — vault.key holds no key material. The tenant predicate is part of the SQL (getKeyForTenant), so the ownership check cannot be skipped by a caller that forgets it. A key belonging to another tenant answers 404, never 403: a 403 would confirm the id EXISTS somewhere, which is precisely what a caller enumerating ids wants, so absence and denial are made indistinguishable. WHY THE PRIMARY CASE IS A 404 AND NOT A 200. The only producer for this endpoint, POST /api/vault/keys, creates a ROOT-tier key, and root/app/pool rows carry tenant_id NULL because they wrap every tenant — so a tenant token deliberately cannot see them, and chaining that id and expecting 200 is unsatisfiable by construction. Reaching a 200 needs a TENANT-tier key, which requires two POSTs to this same endpoint in order (root, then tenant with parent_key_id), and one definition per METHOD+ENDPOINT cannot express that — the constraint recorded in TK-4137. So the case asserts the isolation instead, which is the property actually worth pinning: a root key is invisible to a tenant, and is indistinguishable from a key that does not exist. Flip the first case to 200 once a tenant-tier key id is reachable from cache. Edge cases: a shredded key still reads back for its owner, showing state 'shredded' with shredded_at set and kms_ref NULL, because the row is the audit trail of the erasure; a non-UUID key_id raises 22P02 from the ::uuid cast and surfaces as 500 rather than 400.",
        "operationId": "get_api_vault_keys_key_id",
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "key_id path parameter"
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "no tenant scope — This token carries no tenant, so no key scope can be derived"
          },
          "404": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/consents/check/bulk": {
      "post": {
        "tags": [
          "sdk-consent"
        ],
        "summary": "One granted tuple and one malformed item - the bad item must not fail the batch",
        "description": "The four-tuple consent check for up to 1000 subjects in ONE request and ONE query - the bulk form of POST /api/consents/check, with identical per-tuple semantics (a DISTINCT ON reproduces the single endpoint's ORDER BY granted_at DESC LIMIT 1 per input row). Results are order-preserving and each carries an explicit index, so a caller that filters before zipping cannot silently misalign subjects with verdicts. Failure is per item, never per batch: a malformed item reports ok=false with error_code VALIDATION_ERROR in its own slot while every other subject still returns a verdict, because a campaign check that fails whole is a campaign that silently does not go out. The same tuple repeated in one batch keeps both slots. Envelope-level 400s are reserved for a request that is meaningless rather than partly wrong. Read-only, side-effect-free and safely repeatable; same scope as the single endpoint (consent.check.write).",
        "operationId": "post_api_consents_check_bulk",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — body must be an object with an items[] array"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "person_id": {
                          "type": "string"
                        },
                        "purpose_id": {
                          "type": "string"
                        },
                        "processor": {
                          "type": "string"
                        },
                        "jurisdiction": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "person_id": "<id from auth>",
                    "purpose_id": "<id from consents>",
                    "processor": "tenant",
                    "jurisdiction": "US-CA"
                  },
                  {
                    "person_id": "not-a-uuid",
                    "purpose_id": "<id from consents>",
                    "processor": "tenant",
                    "jurisdiction": "US-CA"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/deliverability/check/bulk": {
      "post": {
        "tags": [
          "sdk-deliverability"
        ],
        "summary": "Mixed email and sms recipients in one call, plus one invalid channel",
        "description": "Pre-send suppression enforcement for up to 1000 recipients in ONE request and ONE query. Distinct from the addresses[] form of POST /api/deliverability/check, which is kept for compatibility but holds tenant and channel fixed across the list and issues a query per address: here every item carries its own channel, so a mixed email+sms audience is a single call. tenant_id may be omitted per item and falls back to the caller's credential. Addresses are never sent to the database in plaintext - each is normalized (email lowercased/trimmed, sms reduced to +digits) and sha256-hashed with the channel as salt, and the query joins on hashes. A row is suppressed when a tenant-scoped or global entry matches the address for that channel (or channel 'all') and has not expired. Results are order-preserving with an explicit index and echo back the address and channel asked about; a malformed item reports ok=false in its own slot rather than failing the batch. The same address may appear more than once and each occurrence keeps its slot. Read-only, side-effect-free and safely repeatable.",
        "operationId": "post_api_deliverability_check_bulk",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — body must be an object with an items[] array"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "tenant_id": {
                          "type": "string"
                        },
                        "channel": {
                          "type": "string"
                        },
                        "address": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "email",
                    "address": "<generated email>"
                  },
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "sms",
                    "address": "+15551234567"
                  },
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "carrier-pigeon",
                    "address": "someone@example.com"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/notifications/send-to-audience": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Send to a role audience under a delegated decision",
        "description": "Sends to an audience named by REFERENCE — a role template or an explicit persona list — never by address, and the response reports per-recipient status keyed by persona_id without ever returning the destination used. This is what lets a consuming app address 'everyone holding this role' when it holds no persona ids and no addresses, without inheriting an erasure surface. `authorization` is a required tagged union: `platform` (the platform decides, against a tenant-registered purpose, and applies the frequency cap), `delegated` (the app already decided; decision_ref and expires_at required), or `exempt` (basis and justification required — an exemption is recorded as a decision, never skipped). Edge cases: a delegated decision whose expires_at has passed is 403 DecisionExpired rather than being honoured, because between deciding and dispatching a consent can be revoked or an address suppressed and every such change runs in the restrictive direction; the platform may only ever NARROW a delegated decision (send -> suppressed/deferred) and never upgrade a denial; `no_destination` is returned per recipient with attempted:false and is NOT a failure and NOT a delivery attempt — retrying it returns the same answer, so a caller's retry ledger must key on `attempted` rather than on the status string; the persona->destination seam is unwired by default, so a platform with no resolver wired reports no_destination for every recipient rather than inventing an address; duplicate persona_ids in an explicit audience are de-duplicated so no caller can cause a double-send; exceeding the recipient ceiling (NOTIFICATION_MAX_AUDIENCE, default 50000) is a 400 naming the ceiling rather than a truncated send.",
        "operationId": "post_api_notifications_send_to_audience",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — authorization is required — a send with no recorded decision is not permitted"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "DecisionExpired — delegated decision <ref> expired at <ts>; re-decide rather than re-send"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "audience": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      },
                      "role_template_id": {
                        "type": "string"
                      }
                    }
                  },
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "body": {
                    "type": "string"
                  },
                  "subject": {
                    "type": "string"
                  },
                  "authorization": {
                    "type": "object",
                    "properties": {
                      "mode": {
                        "type": "string"
                      },
                      "decision_ref": {
                        "type": "string"
                      },
                      "expires_at": {
                        "type": "string"
                      }
                    }
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "audience": {
                  "kind": "role",
                  "role_template_id": "{{var:role_template_id}}"
                },
                "channels": [
                  "email"
                ],
                "body": "An SLA has breached and needs attention.",
                "subject": "SLA breach",
                "authorization": {
                  "mode": "delegated",
                  "decision_ref": "decision-smoke-0001",
                  "expires_at": "2099-01-01T00:00:00Z"
                }
              }
            }
          }
        }
      }
    },
    "/api/notifications/send-window/bulk": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Two channels for one tenant, plus one item with an unsupported channel",
        "description": "The send-window verdict for up to 1000 subjects in one request. The work divides on two different keys and this exploits both: quiet hours are per persona, so one query covers every DISTINCT persona in the batch, while the frequency policy and its usage count are per (tenant, channel, purpose), of which a campaign has one - so a ten-thousand-subject batch issues roughly two queries, not twenty thousand. Evaluating the windows themselves is pure and stays in process. Results are order-preserving with an explicit index; an item with a missing tenant or unsupported channel reports ok=false with error_code VALIDATION_ERROR in its own slot while every other subject still returns a verdict. Each verdict carries the same shape as the single endpoint, including capacity_note - `remaining` is a tenant-wide rolling-24h figure and NOT a per-subject reservation, so a caller must compare it against its own batch size before sending. Nothing is reserved and nothing is sent; the call is read-only and safely repeatable.",
        "operationId": "post_api_notifications_send_window_bulk",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — body must be an object with an items[] array"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Service token is missing required scope: notification.send-window.write"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "tenant_id": {
                          "type": "string"
                        },
                        "channel": {
                          "type": "string"
                        },
                        "purpose": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "email",
                    "purpose": "marketing"
                  },
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "sms",
                    "purpose": "marketing"
                  },
                  {
                    "tenant_id": "<id from auth>",
                    "channel": "smoke-signal"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/notifications/send-window": {
      "post": {
        "tags": [
          "sdk-notification"
        ],
        "summary": "Ask whether an email send is open for a tenant and purpose",
        "description": "Read-only pre-flight: may a send go to this subject on this channel at this moment, and if not, when does the window open. Returns open, reasons (most-blocking first), next_open_at, a quiet_hours block (quiet, reason, and unconfigured so an absent record is distinguishable from an open one) and a frequency block (max_per_day, used_last_24h, remaining, policy_source). channel must be one of email, sms, whatsapp, push, slack. tenant_id falls back to the caller's credential. subject_persona_id is optional and selects whose quiet hours apply; purpose selects the frequency policy, since caps are per purpose so an OTP is not throttled by marketing. An optional ISO-8601 `at` evaluates a future moment for a scheduled campaign - an unparseable value is rejected rather than silently answered for now, because a verdict about the wrong moment is the one failure mode this check must not have. RESERVES NOTHING: it calls getSendUsage, never reserveSend, because a pre-flight that reserved would burn one unit of the daily cap per subject ASKED ABOUT, exhausting the allowance before a message went out and then suppressing the real sends as duplicates. Consequently `remaining` is a tenant-wide rolling-24h figure for the (channel, purpose), not a per-subject reservation - a caller planning a batch must compare it against its own batch size, which the returned capacity_note states. Requires a valid JWT, or a key holding notification.send-window.write.",
        "operationId": "post_api_notifications_send_window",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "VALIDATION_ERROR — tenant_id is required (absent from the request and from the credential)"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "403": {
            "description": "Forbidden — Service token is missing required scope: notification.send-window.write"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tenant_id": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "tenant_id": "<id from auth>",
                "channel": "email",
                "purpose": "marketing"
              }
            }
          }
        }
      }
    },
    "/api/policies/evaluate/bulk": {
      "post": {
        "tags": [
          "sdk-policy"
        ],
        "summary": "Evaluate one policy for the same subject twice - one bare, one with full context",
        "description": "Evaluates up to 1000 (policy, subject) pairs in one request. Collapses the cost that made the per-subject form unusable at campaign scale: ONE policy read per DISTINCT policy_id rather than per item, and ONE batched INSERT for all decision rows. Cedar evaluation stays per item, in process, against an already-compiled term, and the consent gate is the same code path the single endpoint uses so the two cannot disagree about a DENY. Results are order-preserving with an explicit index. An item naming a policy that does not exist reports ok=false with error_code POLICY_NOT_FOUND in its own slot - the single endpoint's 404 is right when the request IS one evaluation, but failing ten thousand verdicts because one named a deleted policy is not. IMPORTANT DIFFERENCE FROM THE SINGLE ENDPOINT: the audit fan-out is aggregated - one policy.evaluated-bulk.v1 per (batch, policy) carrying the allow/deny split, rather than one policy.evaluated.v1 per subject, because N hash-chain appends would serialize the whole batch behind the audit writer. Every individual decision is still written to policy.decision; a caller that needs per-subject audit events must use POST /api/policies/evaluate.",
        "operationId": "post_api_policies_evaluate_bulk",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — body must be an object with an items[] array"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "InternalError — InternalError"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "policy_id": {
                          "type": "string"
                        },
                        "subject_id": {
                          "type": "string"
                        },
                        "context": {
                          "type": "object",
                          "properties": {
                            "subject": {
                              "type": "object",
                              "properties": {
                                "persona": {
                                  "type": "object",
                                  "properties": {
                                    "role": {
                                      "type": "string"
                                    }
                                  }
                                }
                              }
                            },
                            "projection_version": {
                              "type": "integer"
                            }
                          }
                        },
                        "resource_class": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "items": [
                  {
                    "policy_id": "<id from policies>",
                    "subject_id": "<id from auth>",
                    "context": {
                      "subject": {
                        "persona": {
                          "role": "doctor"
                        }
                      },
                      "projection_version": 1
                    },
                    "resource_class": "sensitive"
                  },
                  {
                    "policy_id": "<id from policies>",
                    "subject_id": "<id from auth>"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/api/role-templates/{role_template_id}/holders": {
      "get": {
        "tags": [
          "sdk-persona"
        ],
        "summary": "List role holders for a tenant",
        "description": "The reverse of GET /api/personas/:persona_id/roles: returns the personas holding one role template, scoped to one tenant. Holders are the UNION of two sources — an active persona.role_assignment grant and the persona's own primary_role_template_id — because a tenant that provisions people with a starting template and never edits them would otherwise return an empty list for a role many people hold. Each persona appears AT MOST ONCE even when it holds the role both ways, so a caller fanning out a notification cannot double-send; held_via reports 'assignment' in preference to 'primary'. Suspended/terminated memberships and non-active personas are excluded. Edge cases: tenant_id is a REQUIRED query param and its absence is a 400, not an unscoped read — persona.role_assignment carries no tenant column (the tenant is reached by joining persona.membership), so defaulting it would expose every tenant's role holders; an unknown role_template_id returns 200 with holders = [] rather than 404, since a typo'd template is indistinguishable from a role nobody holds; include_primary is off ONLY for the literal string 'false' (any other value leaves it on, so ?include_primary=true behaves as expected); limit is clamped to 1..1000 with a default of 200; a non-UUID role_template_id or tenant_id fails in Postgres as a 500 because the route does not pre-validate UUID shape.",
        "operationId": "get_api_role_templates_role_template_id_holders",
        "parameters": [
          {
            "name": "role_template_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "role_template_id path parameter"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          },
          "400": {
            "description": "ValidationError — tenant_id query param required"
          },
          "401": {
            "description": "Unauthorized — Missing bearer token"
          },
          "500": {
            "description": "Internal Server Error — Internal Server Error"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKey": []
          }
        ]
      }
    },
    "/api/search/health": {
      "get": {
        "tags": [
          "sdk-search"
        ],
        "summary": "Report search backend availability",
        "description": "Reports whether a search backend is actually wired, so a caller can tell 'search is unavailable here' from 'search ran and matched nothing'. Without it those two are indistinguishable: an unwired deployment 500s every query and a consuming app reasonably renders that as an empty result set, so a shipped feature reports '0 results' for a backend that was never connected — the most misleading answer available, because it looks like a fact about the data. Returns data.search_up (boolean), data.client ('registered' | 'synthetic' | 'fail-loud') and, when unavailable, data.reason. Edge cases: this returns 200 EVEN WHEN search is unavailable, deliberately — it reports a capability state and is not itself failing, so a caller must read data.search_up rather than the status code; answering 503 would make a caller's own health check flap and conflate 'the probe is broken' with 'the thing it probes is down'. It needs no auth (the gateway allowlists any path ending in /health) and discloses nothing tenant-specific, only whether a backend is configured. client='synthetic' means results are fabricated and must not be trusted in production.",
        "operationId": "get_api_search_health",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {}
            }
          }
        },
        "security": []
      }
    }
  }
}
