openapi: 3.1.0
info:
  title: ZeroGrant Platform API
  version: 0.2.0
  description: >
    Platform contract for sandbox and live-gated product surfaces: onboarding,
    projects, keys, policies, clearance intents, webhooks, usage, billing
    reconciliation, compliance evidence, and ops live unlock.
servers:
  - url: http://localhost:4000
security:
  - bearerAuth: []
paths:
  /healthz:
    get:
      security: []
      summary: Liveness
      responses:
        "200":
          description: OK
  /readyz:
    get:
      security: []
      summary: Readiness (database + NTKA)
      responses:
        "200":
          description: Ready
        "503":
          description: Not ready
  /v1/projects:
    get:
      summary: List projects for the authenticated organisation
      responses:
        "200":
          description: Project list
    post:
      summary: Create project with test environment, bootstrap config, and keys
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  minLength: 2
                  maxLength: 80
      responses:
        "201":
          description: Created with one-time secret and publishable keys
  /v1/project:
    get:
      summary: Current project for the authenticated API key
      responses:
        "200":
          description: Project
  /v1/api-keys:
    get:
      summary: List API keys for the current environment
      responses:
        "200":
          description: Key list
    post:
      summary: Create an API key (test or live)
      description: |
        Defaults to the authenticated key mode. Pass mode=live to mint zg_live_/pk_live_
        keys into the org live environment. Requires organisations.live_unlocked_at.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  enum: [secret, publishable]
                name:
                  type: string
                mode:
                  type: string
                  enum: [test, live]
      responses:
        "201":
          description: Created (includes plaintext key once)
        "403":
          description: live_key_creation_gated when mode=live and org is locked
  /v1/api-keys/{key_id}/revoke:
    post:
      summary: Revoke an API key (immediate auth failure)
      parameters:
        - in: path
          name: key_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Revoked
  /v1/webhook-endpoints:
    get:
      summary: List webhook endpoints
      responses:
        "200":
          description: Endpoint list
    post:
      summary: Create webhook endpoint
      responses:
        "201":
          description: Created
  /v1/webhook-endpoints/{endpoint_id}:
    patch:
      summary: Update webhook endpoint URL, enabled flag, or enabled_events
      parameters:
        - in: path
          name: endpoint_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Updated
    delete:
      summary: Delete webhook endpoint and its deliveries
      parameters:
        - in: path
          name: endpoint_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
  /v1/webhook-endpoints/{endpoint_id}/rotate-secret:
    post:
      summary: Rotate webhook endpoint secret (shown once)
      parameters:
        - in: path
          name: endpoint_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: New secret
  /v1/webhook-endpoints/{endpoint_id}/test:
    post:
      summary: Enqueue an endpoint.test delivery to the webhook URL
      parameters:
        - in: path
          name: endpoint_id
          required: true
          schema:
            type: string
      responses:
        "202":
          description: Delivery queued
  /v1/webhook-deliveries:
    get:
      summary: List recent webhook deliveries
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - in: query
          name: starting_after
          schema:
            type: string
          description: Cursor delivery id for the next page
      responses:
        "200":
          description: Delivery list with has_more
  /v1/webhook-deliveries/{delivery_id}/retry:
    post:
      summary: Retry a failed or pending delivery (customer API)
      parameters:
        - in: path
          name: delivery_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Re-queued
  /v1/end-users:
    get:
      summary: List end users
      responses:
        "200":
          description: User list
    post:
      summary: Upsert end user by customer_reference
      responses:
        "200":
          description: Existing
        "201":
          description: Created
  /v1/end-users/{end_user_id}:
    get:
      summary: Get end user
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: End user
  /v1/end-users/{end_user_id}/authority:
    get:
      summary: Sandbox authority summary for end user
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Authority summary
  /v1/end-users/{end_user_id}/freeze:
    post:
      summary: Freeze end user (blocks new intents)
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Frozen
  /v1/end-users/{end_user_id}/unfreeze:
    post:
      summary: Unfreeze end user
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Active
  /v1/end-users/{end_user_id}/authority-sessions:
    get:
      summary: List sandbox authority sessions
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Sessions
    post:
      summary: Create sandbox authority session (not a live Mint Warrant)
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "201":
          description: Created
  /v1/end-users/{end_user_id}/erasure-requests:
    get:
      summary: List sandbox erasure requests
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Erasure requests
    post:
      summary: Record sandbox erasure request (ledger only; freezes user)
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "201":
          description: Recorded
  /v1/audit-events:
    get:
      summary: List redacted product audit events
      responses:
        "200":
          description: Audit events
  /v1/categories:
    get:
      summary: List category definitions for the environment
      responses:
        "200":
          description: Categories
    post:
      summary: Create category (live requires org unlock)
      responses:
        "201":
          description: Created
  /v1/categories/{category_id}:
    get:
      summary: Get category by id or key
      parameters:
        - in: path
          name: category_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Category
  /v1/purposes:
    get:
      summary: List purpose definitions for the environment
      responses:
        "200":
          description: Purposes
    post:
      summary: Create purpose (live requires org unlock)
      responses:
        "201":
          description: Created
  /v1/purposes/{purpose_id}:
    get:
      summary: Get purpose by id or key
      parameters:
        - in: path
          name: purpose_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Purpose
  /v1/adapters:
    get:
      summary: List adapter definitions for the environment
      responses:
        "200":
          description: Adapters
    post:
      summary: Create adapter (live requires org unlock)
      responses:
        "201":
          description: Created
  /v1/adapters/{adapter_id}:
    get:
      summary: Get adapter by id or key
      parameters:
        - in: path
          name: adapter_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Adapter
  /v1/element/config:
    get:
      summary: Publishable-key Element config (pk_test_ or pk_live_)
      responses:
        "200":
          description: Project + elements base URL
  /v1/element/owner/claim-challenge:
    post:
      summary: Element proxy - NTKA owner claim-challenge (client_secret)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [owner_id]
              properties:
                owner_id:
                  type: string
      responses:
        "200":
          description: Child import challenge
        "400":
          description: owner_id_required
        "401":
          description: client_secret required
  /v1/element/owner/register:
    post:
      summary: Element proxy - NTKA owner register (no Parent fields allowed)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [owner_id]
              properties:
                owner_id:
                  type: string
                child_id:
                  type: string
                child_public_key:
                  type: string
              additionalProperties: true
              description: Child registration payload; Parent/recovery/PRF keys forbidden
      responses:
        "200":
          description: Registered
        "202":
          description: Accepted
        "400":
          description: owner_id_required or parent_material_forbidden
        "401":
          description: client_secret required
  /v1/element/owner/state:
    get:
      summary: Element proxy - NTKA owner state
      parameters:
        - in: query
          name: owner_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Owner state
  /v1/element/session:
    get:
      summary: Load ClearanceIntent for Element (Bearer ci_secret_...)
      responses:
        "200":
          description: Redacted intent session
  /v1/element/session/approve:
    post:
      summary: Element approve via client_secret (sandbox simulate or live Mint Warrant)
      description: |
        Test mode with SANDBOX_SIMULATED_OWNER may omit the body (simulated owner).
        Live mode requires mint_warrant + signature_hex + owner_assertion
        (ceremony + credential_id). Parent never sent. Warrant must match intent
        scopes and registered Owner Child.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                mint_warrant:
                  type: object
                  description: NTKA-registerable Mint Warrant object
                signature_hex:
                  type: string
                owner_assertion:
                  type: object
                  properties:
                    ceremony:
                      type: string
                      example: webauthn_prf_v1
                    credential_id:
                      type: string
      responses:
        "200":
          description: Succeeded
        "403":
          description: |
            owner_action_required, simulate_approve_test_only, or
            mint_warrant_intent_mismatch (warrant not bound to this intent)
  /v1/element/session/decline:
    post:
      summary: Decline pending intent via client_secret
      responses:
        "200":
          description: Cancelled
  /v1/clearance-intents:
    get:
      summary: List clearance intents
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - in: query
          name: starting_after
          schema:
            type: string
          description: Cursor intent id for the next page
      responses:
        "200":
          description: Intent list with has_more
    post:
      summary: Create clearance intent (returns client_secret once)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_reference:
                  type: string
                purpose:
                  type: string
                categories:
                  type: array
                  items:
                    type: string
                adapter:
                  type: string
                expires_in:
                  type: integer
                  minimum: 1
                  maximum: 86400
                  description: Seconds until intent expires (default 600)
                idempotency_key:
                  type: string
      parameters:
        - in: header
          name: Idempotency-Key
          required: false
          schema:
            type: string
            minLength: 8
            maxLength: 128
          description: Preferred over body idempotency_key; replay returns original create payload
      responses:
        "201":
          description: Created with client_secret
        "200":
          description: Idempotent replay of original create payload
        "409":
          description: Idempotency-Key reused with a different request body
  /v1/clearance-intents/{intent_id}:
    get:
      summary: Get clearance intent (+ result when present)
      parameters:
        - in: path
          name: intent_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Intent
  /v1/clearance-intents/{intent_id}/result:
    get:
      summary: Get bounded clearance result (secret key only)
      parameters:
        - in: path
          name: intent_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Result
  /v1/clearance-intents/{intent_id}/simulate-approve:
    post:
      summary: Sandbox simulated-owner approve via real NTKA Mint Warrant + Grant (test mode only)
      parameters:
        - in: path
          name: intent_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Succeeded
  /v1/clearance-intents/{intent_id}/cancel:
    post:
      summary: Cancel a pending intent
      parameters:
        - in: path
          name: intent_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Cancelled
  /v1/usage:
    get:
      summary: Usage summary for current billing period
      responses:
        "200":
          description: Usage
  /v1/usage/end-users/{end_user_id}:
    get:
      summary: Usage for one end user in the current period
      parameters:
        - in: path
          name: end_user_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Per-user usage
  /v1/onboarding/status:
    get:
      summary: Organisation membership status for a Supabase session
      responses:
        "200":
          description: Status
        "401":
          description: Missing or invalid session
  /v1/onboarding/bootstrap:
    post:
      summary: Self-serve org + project + test keys (Supabase session)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [organisation_name]
              properties:
                organisation_name:
                  type: string
                project_name:
                  type: string
                billing_email:
                  type: string
      responses:
        "201":
          description: Provisioned with one-time keys
        "409":
          description: Already provisioned
  /v1/policies:
    get:
      summary: List clearance policies for the environment
      responses:
        "200":
          description: Policy list
    post:
      summary: Create draft or published policy version
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  minLength: 2
                  maxLength: 80
                category_keys:
                  type: array
                  items:
                    type: string
                purpose_key:
                  type: string
                adapter_key:
                  type: string
                operations:
                  type: array
                  items:
                    type: string
                grant_ttl_seconds:
                  type: integer
                  minimum: 60
                  maximum: 600
                result_retention_days:
                  type: integer
                  minimum: 1
                  maximum: 3650
                preview_required:
                  type: boolean
                publish:
                  type: boolean
      responses:
        "201":
          description: Created with owner_preview
  /v1/policies/{id}/publish:
    post:
      summary: Publish a draft clearance policy
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Published
        "404":
          description: Draft not found
  /v1/compliance-evidence:
    get:
      summary: Download Compliance Evidence Pack JSON
      responses:
        "200":
          description: Evidence pack (zerogrant-evidence/v1)
  /v1/billing/invoices:
    get:
      summary: Reconciliation invoices from usage_ledger (sandbox-safe)
      responses:
        "200":
          description: Invoice drafts + outbox reconciliation
  /v1/billing:
    get:
      summary: Billing customer + outbox snapshot
      responses:
        "200":
          description: Billing
  /v1/system/ntka:
    get:
      summary: Probe NTKA readyz + authenticated ops health
      responses:
        "200":
          description: Probe result
  /v1/system/billing-outbox-dry-run:
    post:
      summary: Enqueue a dry-run meter outbox row (idempotent)
      responses:
        "200":
          description: Outbox row
  /v1/ops/overview:
    get:
      summary: Ops staff overview counters
      responses:
        "200":
          description: Counts
        "401":
          description: Unauthorized
  /v1/ops/organisations:
    get:
      summary: List organisations (includes live_unlocked_at)
      responses:
        "200":
          description: Org list
  /v1/ops/organisations/{org_id}/live-unlock:
    post:
      summary: Unlock organisation for live mode writes
      parameters:
        - in: path
          name: org_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Unlocked
        "404":
          description: Org not found
  /v1/ops/organisations/{org_id}/live-lock:
    post:
      summary: Lock organisation (clear live_unlocked_at)
      parameters:
        - in: path
          name: org_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Locked
        "404":
          description: Org not found
  /v1/ops/ntka-health:
    get:
      summary: Ops NTKA readiness probe
      responses:
        "200":
          description: NTKA health payload
        "401":
          description: Unauthorized
  /v1/ops/webhook-deliveries:
    get:
      summary: List recent webhook deliveries across orgs
      responses:
        "200":
          description: Delivery list
        "401":
          description: Unauthorized
  /v1/ops/webhook-deliveries/{delivery_id}/retry:
    post:
      summary: Re-queue a failed webhook delivery
      parameters:
        - in: path
          name: delivery_id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Retry accepted
        "404":
          description: Delivery not found
  /v1/ops/billing-outbox:
    get:
      summary: Inspect billing outbox rows (sandbox never charges)
      responses:
        "200":
          description: Outbox rows
        "401":
          description: Unauthorized
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        Environment secret API key (zg_test_... / zg_live_...) or a Supabase
        access token from email OTP. Dashboard/onboarding requires organisation_members;
        /v1/ops/* requires ops_staff for the same session subject.
