openapi: 3.1.0
info:
  title: QairoPay API
  version: "2026-05-19"
  summary: Wallet passes and stablecoin-settled cards, in one API.
  description: |
    The QairoPay API issues white-label wallet passes for Apple Wallet and
    Google Wallet, runs a debit-style Spend Card program via a sponsor bank,
    and settles eligible flows in USDC on Aptos.

    This document is the canonical contract. The TypeScript SDK is generated
    against it; the public reference site is rendered from it; CI validates
    every server response against it.
  termsOfService: https://qairopay.com/terms/
  contact:
    name: QairoPay Developers
    email: developers@qairopay.com
    url: https://developers.qairopay.com
  license:
    name: Proprietary
    url: https://qairopay.com/terms/

servers:
  - url: https://api.qairopay.com
    description: Production
  - url: https://api.sandbox.qairopay.com
    description: Sandbox

security:
  - bearerAuth: []

tags:
  - name: Passes
    description: Wallet passes — templates, issuance, lifecycle.
  - name: Pass templates
    description: The visual and behavioral mold for issued passes.
  - name: Cards
    description: Spend Card issuance, lifecycle, and provisioning.
  - name: Cardholders
    description: End users of card programs; subject to KYC.
  - name: Transactions
    description: Card-network transactions on issued cards.
  - name: Disputes
    description: Cardholder dispute lifecycle.
  - name: Settlements
    description: USD-to-USDC settlements via the on-ramp adapter.
  - name: Payouts
    description: USDC and fiat off-ramp payouts.
  - name: Webhooks
    description: Webhook endpoint management and event types.

paths:
  /v1/me:
    get:
      operationId: getMe
      tags: [Passes]
      summary: Retrieve the authenticated tenant
      description: Returns the tenant the calling API key is scoped to. Useful as a health-check.
      responses:
        "200":
          description: The authenticated tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
              examples:
                default:
                  value:
                    tenant:
                      id: tnt_01HZX
                      name: Acme Hotels
                      mode: live
                    key:
                      id: key_01HZX
                      scope: write

  /v1/pass_templates:
    post:
      operationId: createPassTemplate
      tags: [Pass templates]
      summary: Create a pass template
      description: Create a re-usable visual and behavioral mold for issued passes. Templates are tenant-scoped; promotion from sandbox to live is a separate verb.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PassTemplateCreate"
      responses:
        "201":
          description: Created template.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PassTemplate"
        "400":
          $ref: "#/components/responses/BadRequest"
    get:
      operationId: listPassTemplates
      tags: [Pass templates]
      summary: List pass templates
      description: Return a cursor-paginated page of pass templates owned by the calling tenant.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
      responses:
        "200":
          description: A page of templates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PassTemplateList"

  /v1/passes:
    post:
      operationId: issuePass
      tags: [Passes]
      summary: Issue a pass from a template
      description: Issue a new wallet pass for an end user. Returns Apple Wallet and Google Wallet download URLs; the pass becomes `installed` when the holder adds it to a wallet.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PassCreate"
      responses:
        "201":
          description: The issued pass with download URLs.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pass"
              examples:
                default:
                  value:
                    id: pass_01HZX
                    template_id: tpl_01HZX
                    status: issued
                    holder:
                      email: jane@example.com
                      name: Jane Doe
                    download:
                      apple_url: https://passes.qairopay.com/.../apple.pkpass
                      google_url: https://pay.google.com/gp/v/save/eyJ0eXA...
                    created_at: 2026-05-19T10:14:22Z
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          $ref: "#/components/responses/IdempotencyConflict"
    get:
      operationId: listPasses
      tags: [Passes]
      summary: List passes
      description: Return a cursor-paginated page of passes. Filter by `status` or `template_id`.
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - in: query
          name: status
          schema:
            type: string
            enum: [issued, installed, revoked, removed, archived]
        - in: query
          name: template_id
          schema: { type: string }
      responses:
        "200":
          description: A page of passes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PassList"

  /v1/passes/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      operationId: getPass
      tags: [Passes]
      summary: Retrieve a pass
      description: Fetch a pass by ID. Returns 404 if the pass does not exist in the calling tenant; cross-tenant access is never reported.
      responses:
        "200":
          description: The pass.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pass" }
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      operationId: updatePass
      tags: [Passes]
      summary: Update a pass
      description: Patch fields on an issued pass. By default the update is pushed silently to the holder's wallet; include `change_message` to surface a user-visible notification.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PassUpdate" }
      responses:
        "200":
          description: The updated pass.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pass" }

  /v1/passes/{id}/revoke:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    post:
      operationId: revokePass
      tags: [Passes]
      summary: Revoke an issued pass
      description: Revoke a pass — the wallet voids it (Apple greys with a "Voided" stamp; Google hides it) and scanners reject it. Idempotent on `(tenant, pass_id)`; revoking an already-revoked pass is a no-op.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, example: lost_device }
      responses:
        "200":
          description: The revoked pass.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Pass" }

  /v1/cardholders:
    post:
      operationId: createCardholder
      tags: [Cardholders]
      summary: Create a cardholder
      description: Create a cardholder record. The first card issued to this cardholder triggers KYC verification via the configured identity provider (Persona).
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CardholderCreate" }
      responses:
        "201":
          description: The cardholder, including KYC status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Cardholder" }

  /v1/cards:
    post:
      operationId: issueCard
      tags: [Cards]
      summary: Issue a card to a cardholder
      description: Issue a virtual or physical Spend Card. PAN/CVC are never returned by this endpoint — call the card display-token endpoint and render via the hosted iframe to surface card details to the cardholder.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CardCreate" }
      responses:
        "201":
          description: The issued card (PAN never returned).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Card" }

  /v1/cards/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      operationId: getCard
      tags: [Cards]
      summary: Retrieve a card
      description: Fetch a card by ID. Returns last-4, expiry, status, and spending controls — never the PAN or CVC.
      responses:
        "200":
          description: The card.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Card" }

  /v1/settlements/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      operationId: getSettlement
      tags: [Settlements]
      summary: Retrieve a settlement
      description: Fetch a settlement by ID. Includes the on-chain transaction hash on Aptos, the Bridge on-ramp reference, and the per-line breakdown of contributing card transactions.
      responses:
        "200":
          description: The settlement with on-chain reference.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Settlement" }

  /v1/payouts:
    post:
      operationId: createPayout
      tags: [Payouts]
      summary: Create a payout (fiat off-ramp or on-chain USDC)
      description: Create a fiat off-ramp (USDC → bank wire) or an on-chain USDC payout to an allowlisted Aptos address. Sanctions and Travel Rule screens run before broadcast.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PayoutCreate" }
      responses:
        "201":
          description: The created payout.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }

  /v1/webhook_endpoints:
    post:
      operationId: createWebhookEndpoint
      tags: [Webhooks]
      summary: Register a webhook endpoint
      description: Register a URL to receive event deliveries. The response includes a one-time-visible `signing_secret` — store it immediately in your secret manager; QairoPay never returns it again.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEndpointCreate" }
      responses:
        "201":
          description: The created endpoint, including the one-time-visible signing secret.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookEndpoint" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key (prefix indicates env and role)

  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: true
      schema:
        type: string
        maxLength: 255
        pattern: "^[A-Za-z0-9_\\-]+$"
      description: A unique key per logical operation. Retries with the same key return the original response. See https://developers.qairopay.com/concepts/idempotency/.
    Limit:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    StartingAfter:
      in: query
      name: starting_after
      schema: { type: string }
      description: Cursor from a previous response's `next_cursor`.
    IdPath:
      in: path
      name: id
      required: true
      schema: { type: string }

  responses:
    BadRequest:
      description: The request body failed validation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: The resource does not exist within the calling tenant.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    IdempotencyConflict:
      description: The same Idempotency-Key was reused with a different body or is still in flight.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, message]
          properties:
            type:
              type: string
              enum:
                [
                  invalid_parameter,
                  unauthenticated,
                  insufficient_scope,
                  not_found,
                  idempotency_conflict,
                  idempotency_inflight,
                  conflict,
                  rate_limited,
                  validation_failed,
                  card_declined,
                  provider_error,
                  internal_error,
                  service_unavailable,
                ]
            code: { type: string }
            message: { type: string }
            param: { type: string, description: "JSON pointer to the offending field" }
            request_id: { type: string }
            docs_url: { type: string, format: uri }
            retry_after_ms: { type: integer }

    MeResponse:
      type: object
      properties:
        tenant:
          type: object
          properties:
            id: { type: string, example: tnt_01HZX }
            name: { type: string, example: Acme Hotels }
            mode: { type: string, enum: [sandbox, live] }
        key:
          type: object
          properties:
            id: { type: string, example: key_01HZX }
            scope: { type: string, enum: [read, write, admin] }

    Brand:
      type: object
      properties:
        background_color: { type: string, example: "#1F7A5A" }
        foreground_color: { type: string, example: "#FCFAF6" }
        logo_url: { type: string, format: uri }
        icon_url: { type: string, format: uri }

    PassField:
      type: object
      required: [key, label, value]
      properties:
        key: { type: string }
        label: { type: string }
        value: { type: string }
        display: { type: string, enum: [primary, secondary, auxiliary, header, back] }
        numeric: { type: boolean }

    PassTemplateCreate:
      type: object
      required: [name, kind]
      properties:
        name: { type: string }
        kind:
          type: string
          enum: [loyalty, membership, coupon, event_ticket, boarding_pass, gift_card]
        brand: { $ref: "#/components/schemas/Brand" }
        fields:
          type: array
          items: { $ref: "#/components/schemas/PassField" }
        barcode:
          type: object
          properties:
            format: { type: string, enum: [qr, pdf417, aztec, code128] }
            payload_template: { type: string, example: "loyalty:{pass.id}" }
        nfc:
          type: object
          properties:
            enabled: { type: boolean }
            payload_template: { type: string }
        relevant_locations:
          type: array
          items:
            type: object
            properties:
              latitude: { type: number, format: double }
              longitude: { type: number, format: double }
              radius_m: { type: integer }
              message: { type: string }

    PassTemplate:
      allOf:
        - $ref: "#/components/schemas/PassTemplateCreate"
        - type: object
          required: [id, created_at]
          properties:
            id: { type: string, example: tpl_01HZX }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }

    PassTemplateList:
      type: object
      required: [data, has_more]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/PassTemplate" }
        has_more: { type: boolean }
        next_cursor: { type: string, nullable: true }

    Holder:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        phone: { type: string }

    PassCreate:
      type: object
      required: [template_id, holder]
      properties:
        template_id: { type: string }
        holder: { $ref: "#/components/schemas/Holder" }
        fields:
          type: object
          additionalProperties: { type: string }

    PassUpdate:
      type: object
      properties:
        fields:
          type: object
          additionalProperties: { type: string }
        change_message: { type: string }

    Pass:
      type: object
      required: [id, template_id, status]
      properties:
        id: { type: string, example: pass_01HZX }
        template_id: { type: string }
        status:
          type: string
          enum: [issued, installed, revoked, removed, archived]
        holder: { $ref: "#/components/schemas/Holder" }
        fields:
          type: object
          additionalProperties: { type: string }
        download:
          type: object
          properties:
            apple_url: { type: string, format: uri }
            google_url: { type: string, format: uri }
        revoke_reason: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    PassList:
      type: object
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/Pass" }
        has_more: { type: boolean }
        next_cursor: { type: string, nullable: true }

    Address:
      type: object
      required: [line1, city, country]
      properties:
        line1: { type: string }
        line2: { type: string }
        city: { type: string }
        state: { type: string }
        postal_code: { type: string }
        country: { type: string, example: "US", minLength: 2, maxLength: 2 }

    CardholderCreate:
      type: object
      required: [name, email, date_of_birth, address]
      properties:
        name: { type: string }
        email: { type: string, format: email }
        phone: { type: string }
        date_of_birth: { type: string, format: date }
        address: { $ref: "#/components/schemas/Address" }

    Cardholder:
      allOf:
        - $ref: "#/components/schemas/CardholderCreate"
        - type: object
          properties:
            id: { type: string, example: ch_01HZX }
            kyc:
              type: object
              properties:
                status: { type: string, enum: [pending, verified, requires_action, rejected] }
                completed_at: { type: string, format: date-time, nullable: true }
            created_at: { type: string, format: date-time }

    SpendingControls:
      type: object
      properties:
        daily_limit_cents: { type: integer }
        monthly_limit_cents: { type: integer }
        blocked_categories:
          type: array
          items: { type: string }
        blocked_merchants:
          type: array
          items: { type: string }

    CardCreate:
      type: object
      required: [cardholder_id, type]
      properties:
        cardholder_id: { type: string }
        program: { type: string, default: default }
        type: { type: string, enum: [virtual, physical] }
        spending_controls: { $ref: "#/components/schemas/SpendingControls" }
        shipping:
          type: object
          properties:
            service: { type: string, enum: [standard, express] }
            address: { $ref: "#/components/schemas/Address" }

    Card:
      type: object
      properties:
        id: { type: string, example: card_01HZX }
        cardholder_id: { type: string }
        type: { type: string, enum: [virtual, physical] }
        status: { type: string, enum: [pending, active, suspended, terminated] }
        last4: { type: string, example: "4092" }
        expiry_month: { type: integer, minimum: 1, maximum: 12 }
        expiry_year: { type: integer }
        spending_controls: { $ref: "#/components/schemas/SpendingControls" }
        created_at: { type: string, format: date-time }

    SettlementLine:
      type: object
      properties:
        type: { type: string, enum: [card_transaction, pass_top_up, adjustment] }
        txn_id: { type: string }
        amount_cents: { type: integer }

    Settlement:
      type: object
      properties:
        id: { type: string, example: set_01HZX }
        status: { type: string, enum: [scheduled, in_progress, completed, failed, manual_review] }
        currency: { type: string, example: USD }
        amount_cents: { type: integer }
        usdc_amount: { type: string, example: "2180.43" }
        fee_cents: { type: integer }
        net_cents: { type: integer }
        on_ramp:
          type: object
          properties:
            provider: { type: string, example: bridge }
            reference: { type: string }
            rate: { type: string }
        on_chain:
          type: object
          properties:
            network: { type: string, example: aptos }
            tx_hash: { type: string }
            block_height: { type: integer }
        lines:
          type: array
          items: { $ref: "#/components/schemas/SettlementLine" }
        completed_at: { type: string, format: date-time, nullable: true }

    PayoutCreate:
      type: object
      properties:
        amount_cents: { type: integer }
        amount_usdc: { type: string }
        destination:
          oneOf:
            - type: object
              properties:
                bank_account_id: { type: string }
            - type: object
              properties:
                aptos_address: { type: string }
        speed: { type: string, enum: [next_business_day, two_business_days] }

    Payout:
      allOf:
        - $ref: "#/components/schemas/PayoutCreate"
        - type: object
          properties:
            id: { type: string, example: po_01HZX }
            status: { type: string, enum: [pending, in_transit, completed, failed] }
            created_at: { type: string, format: date-time }

    WebhookEndpointCreate:
      type: object
      required: [url, enabled_events]
      properties:
        url: { type: string, format: uri }
        enabled_events:
          type: array
          items: { type: string }
        description: { type: string }

    WebhookEndpoint:
      allOf:
        - $ref: "#/components/schemas/WebhookEndpointCreate"
        - type: object
          properties:
            id: { type: string, example: whk_01HZX }
            signing_secret: { type: string, description: "Shown once on creation, never again." }
            created_at: { type: string, format: date-time }
