openapi: 3.1.0
info:
  title: Caibo Global Payouts API
  version: "1.0.0-block2"
  description: >
    Public B2B payouts API. Organizations authenticate with API keys and send
    stablecoin payouts (live). Fiat rails — pay-in via Interac e-Transfer/ACH/EFT
    (Collections) and local-currency payout via licensed partners (Fiat Payouts)
    — are rolling out; their operations are marked "Rolling out" and return
    503 provider_unavailable until enabled for an organization. Amounts are
    decimal strings; timestamps are ISO 8601; IDs are prefixed
    (org_, po_, col_, fpo_, key_, whe_). Errors use a Stripe-style envelope.
    This spec is the source of truth for the /v1 contract.
servers:
  - url: https://api.caiboglobal.com
security:
  - ApiKeyAuth: []
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "Live/test key, e.g. ck_live_... or ck_test_..."
    FirebaseAuth:
      type: http
      scheme: bearer
      description: Firebase ID token — only for POST /v1/organizations (onboarding).
  schemas:
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum: [invalid_request_error, authentication_error, permission_error, rate_limit_error, compliance_error, api_error]
            code: { type: string }
            message: { type: string }
            request_id: { type: string }
    Destination:
      type: object
      required: [network, address]
      properties:
        network:
          type: string
          enum: [polygon, arbitrum, optimism, base, bsc, ethereum, tron, solana]
        address: { type: string }
    Payout:
      type: object
      properties:
        id: { type: string, example: po_1a2b }
        object: { type: string, const: payout }
        status:
          type: string
          enum: [pending, screening, approved, processing, completed, failed, blocked, cancelled, pending_approval]
        mode: { type: string, enum: [test, live] }
        amount: { type: string, example: "250.00" }
        currency: { type: string, enum: [USDC, USDT, ETH] }
        rail: { type: string, enum: [stablecoin] }
        destination: { $ref: "#/components/schemas/Destination" }
        screening_status: { type: string, enum: [pending, clear, blocked] }
        tx_hash: { type: [string, "null"] }
        failure_reason: { type: string }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }
    Organization:
      type: object
      properties:
        id: { type: string, example: org_9 }
        object: { type: string, const: organization }
        legal_name: { type: string }
        display_name: { type: string }
        country: { type: string }
        status: { type: string, enum: [pending, active, suspended, closed] }
        kyb_status: { type: string, enum: [unverified, pending, verified, rejected] }
        created_at: { type: string, format: date-time }
    Collection:
      description: Inbound fiat pay-in that settles to USDC on the org balance. Rolling out.
      type: object
      properties:
        id: { type: string, example: col_7a1b }
        object: { type: string, const: collection }
        status:
          type: string
          enum: [awaiting_payment, received, settled, failed, expired, cancelled]
        mode: { type: string, enum: [test, live] }
        amount: { type: string, example: "500.00" }
        currency: { type: string, example: CAD }
        method: { type: string, enum: [interac_etransfer, ach, eft] }
        payment_instructions:
          type: object
          additionalProperties: true
          description: Method-specific instructions to present to the payer (e.g. Interac target or virtual account).
        customer_reference: { type: string }
        metadata: { type: object, additionalProperties: true }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
    FiatPayout:
      description: Outbound local-currency payout to a beneficiary, funded from the USDC balance, via a licensed partner. Rolling out.
      type: object
      properties:
        id: { type: string, example: fpo_9a8b }
        object: { type: string, const: fiat_payout }
        status:
          type: string
          enum: [pending, screening, processing, completed, failed, blocked, cancelled, pending_approval]
        mode: { type: string, enum: [test, live] }
        amount: { type: string, example: "250000.00" }
        currency: { type: string, example: COP }
        country: { type: string, description: ISO-3166 alpha-2 }
        beneficiary:
          type: object
          required: [name, account]
          properties:
            name: { type: string }
            account:
              type: object
              additionalProperties: true
              description: Local account details; required keys vary by country/method (see GET /v1/payment-methods). account_number is masked in responses.
        screening_status: { type: string, enum: [pending, clear, blocked] }
        tracking: { type: object, additionalProperties: true }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }
paths:
  /v1/organizations:
    post:
      summary: Onboard an organization (starts KYB, mints a test key)
      security:
        - FirebaseAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [legal_name, country]
              properties:
                legal_name: { type: string }
                display_name: { type: string }
                country: { type: string, description: ISO-3166 alpha-2 }
      responses:
        "201":
          description: Created (api_key.key is shown once)
          content:
            application/json:
              schema:
                type: object
                properties:
                  organization: { $ref: "#/components/schemas/Organization" }
                  api_key:
                    type: object
                    properties:
                      object: { type: string, const: api_key }
                      prefix: { type: string }
                      mode: { type: string }
                      key: { type: string, description: plaintext, shown once }
        "409": { description: Conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/organization:
    get:
      summary: Get the authenticated organization
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Organization" } } } }
  /v1/kyb:
    post:
      summary: Submit KYB (requires the organization:write scope)
      description: >
        Submits the organization's KYB (legal details, UBOs, document
        references) and moves it to pending review. Blocked while already
        verified or under review.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [legal_name, registration_no, incorporation_country, beneficial_owners, documents]
              properties:
                legal_name: { type: string }
                registration_no: { type: string }
                incorporation_country: { type: string, description: ISO-3166 alpha-2 }
                beneficial_owners:
                  type: array
                  items:
                    type: object
                    required: [name, dob, ownership_pct, country]
                    properties:
                      name: { type: string }
                      dob: { type: string, format: date }
                      ownership_pct: { type: string, description: "0 < pct <= 100; sum <= 100" }
                      country: { type: string }
                documents:
                  type: array
                  items:
                    type: object
                    required: [type, reference]
                    properties:
                      type: { type: string }
                      reference: { type: string, description: URL/storage reference }
      responses:
        "201": { description: Submitted (status pending) }
        "409": { description: Not resubmittable (verified/in-flight), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    get:
      summary: Get the org's latest KYB submission (organization:read)
      responses:
        "200": { description: OK }
        "404": { description: No submission, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/payouts:
    post:
      summary: Create a payout
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, destination]
              properties:
                amount: { type: string }
                currency: { type: string, enum: [USDC, USDT, ETH] }
                rail: { type: string, enum: [stablecoin], default: stablecoin }
                destination: { $ref: "#/components/schemas/Destination" }
                metadata: { type: object, additionalProperties: true }
      responses:
        "201": { description: Created/processing, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } }
        "202": { description: Over threshold — pending_approval, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } }
        "402": { description: Insufficient balance, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { description: KYB required / screening blocked / limit exceeded, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: Idempotency conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "422": { description: Unsupported destination, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    get:
      summary: List payouts (cursor pagination)
      parameters:
        - { in: query, name: limit, schema: { type: integer, default: 25, maximum: 100 } }
        - { in: query, name: starting_after, schema: { type: string } }
        - { in: query, name: status, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Payout" } }
                  has_more: { type: boolean }
                  next_cursor: { type: [string, "null"] }
  /v1/payouts/{id}:
    get:
      summary: Retrieve a payout
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } }
        "404": { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/payouts/{id}/cancel:
    post:
      summary: Cancel a payout (only before processing)
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses:
        "200": { description: Cancelled, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } }
        "409": { description: Not cancelable, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/collections:
    post:
      summary: "Create a collection (fiat pay-in). Rolling out."
      description: >
        Rolling out — not yet available in production; returns 503
        provider_unavailable until the collection rail is enabled for the
        organization. Creates a fiat pay-in and returns method-specific payment
        instructions (Interac e-Transfer, ACH, or EFT).
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, method]
              properties:
                amount: { type: string }
                currency: { type: string, example: CAD }
                method: { type: string, enum: [interac_etransfer, ach, eft] }
                customer_reference: { type: string }
                metadata: { type: object, additionalProperties: true }
      responses:
        "201": { description: Created (awaiting_payment), content: { application/json: { schema: { $ref: "#/components/schemas/Collection" } } } }
        "403": { description: KYB required, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: Idempotency conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "422": { description: Unsupported method/currency, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "503": { description: Provider unavailable (rolling out), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    get:
      summary: "List collections (cursor pagination). Rolling out."
      description: Rolling out — see POST /v1/collections.
      parameters:
        - { in: query, name: limit, schema: { type: integer, default: 25, maximum: 100 } }
        - { in: query, name: starting_after, schema: { type: string } }
        - { in: query, name: status, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/Collection" } }
                  has_more: { type: boolean }
                  next_cursor: { type: [string, "null"] }
  /v1/collections/{id}:
    get:
      summary: "Retrieve a collection. Rolling out."
      description: Rolling out — see POST /v1/collections.
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Collection" } } } }
        "404": { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/fiat-payouts:
    post:
      summary: "Create a fiat payout (local-currency disbursement). Rolling out."
      description: >
        Rolling out — not yet available in production; returns 503
        provider_unavailable until the payout corridor is enabled for the
        organization. Screens the beneficiary, funds from the USDC balance, and
        delivers local currency via a licensed payout partner.
      parameters:
        - in: header
          name: Idempotency-Key
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, currency, country, beneficiary]
              properties:
                amount: { type: string }
                currency: { type: string, example: COP }
                country: { type: string, description: ISO-3166 alpha-2 }
                beneficiary:
                  type: object
                  required: [name, account]
                  properties:
                    name: { type: string }
                    account: { type: object, additionalProperties: true, description: "Required keys vary by country/method (see GET /v1/payment-methods)." }
                metadata: { type: object, additionalProperties: true }
      responses:
        "201": { description: Created/processing, content: { application/json: { schema: { $ref: "#/components/schemas/FiatPayout" } } } }
        "202": { description: Over threshold — pending_approval, content: { application/json: { schema: { $ref: "#/components/schemas/FiatPayout" } } } }
        "402": { description: Insufficient balance, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "403": { description: KYB required / screening blocked / limit exceeded, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: Idempotency conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "422": { description: Unsupported country/currency or missing beneficiary fields, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "503": { description: Provider unavailable (rolling out), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    get:
      summary: "List fiat payouts (cursor pagination). Rolling out."
      description: Rolling out — see POST /v1/fiat-payouts.
      parameters:
        - { in: query, name: limit, schema: { type: integer, default: 25, maximum: 100 } }
        - { in: query, name: starting_after, schema: { type: string } }
        - { in: query, name: status, schema: { type: string } }
        - { in: query, name: country, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data: { type: array, items: { $ref: "#/components/schemas/FiatPayout" } }
                  has_more: { type: boolean }
                  next_cursor: { type: [string, "null"] }
  /v1/fiat-payouts/{id}:
    get:
      summary: "Retrieve a fiat payout. Rolling out."
      description: Rolling out — see POST /v1/fiat-payouts.
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/FiatPayout" } } } }
        "404": { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  /v1/balances:
    get:
      summary: Org balances per asset
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, const: list }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        asset: { type: string }
                        available: { type: string }
                        held: { type: string }
  /v1/rates:
    get:
      summary: Conversion quote (read-only)
      parameters:
        - { in: query, name: from_currency_id, required: true, schema: { type: string } }
        - { in: query, name: to_currency_id, required: true, schema: { type: string } }
        - { in: query, name: amount, schema: { type: string } }
      responses:
        "200": { description: OK }
  /v1/countries:
    get: { summary: Supported countries, responses: { "200": { description: OK } } }
  /v1/payment-methods:
    get: { summary: Supported payment methods, responses: { "200": { description: OK } } }
  /v1/webhooks:
    post:
      summary: Register a webhook endpoint (secret shown once)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, description: https only }
                events:
                  type: array
                  items: { type: string, enum: [payout.created, payout.processing, payout.completed, payout.failed, payout.blocked, collection.received, collection.settled, collection.failed, collection.expired, fiat_payout.processing, fiat_payout.completed, fiat_payout.failed, fiat_payout.blocked] }
      responses:
        "201": { description: Created }
    get:
      summary: List webhook endpoints
      responses: { "200": { description: OK } }
  /v1/webhooks/{id}:
    delete:
      summary: Delete a webhook endpoint
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses: { "204": { description: Deleted } }
  /v1/api-keys:
    post:
      summary: Create an API key (live keys require verified KYB)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                mode: { type: string, enum: [test, live] }
                name: { type: string }
                scopes: { type: array, items: { type: string } }
      responses: { "201": { description: Created (key shown once) } }
    get:
      summary: List API keys (metadata only)
      responses: { "200": { description: OK } }
  /v1/api-keys/{id}:
    delete:
      summary: Revoke an API key
      parameters: [ { in: path, name: id, required: true, schema: { type: string } } ]
      responses: { "204": { description: Revoked } }
