openapi: 3.1.0
info:
  title: Keette API
  version: "1.0.0"
  description: |
    Public HTTP API for Keette workspaces. One base URL — live vs sandbox is
    selected by your API key (`sk_live_…` / `sk_test_…`).

    Endpoints are added as each implementation phase ships.
  contact:
    name: Keette Support
    email: talk@keette.com
servers:
  - url: https://20b9-41-90-209-232.ngrok-free.app/v1
    description: Local (ngrok)
  - url: https://api.keette.com/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Account
    description: Identity for the authenticating API key
  - name: Balances
    description: Wallet books
  - name: Beneficiaries
    description: Payout recipients
  - name: Transactions
    description: Unified transaction index
  - name: FX
    description: Indicative rates and firm quotes
  - name: Catalog
    description: Payout corridors and relationships
  - name: Payouts
    description: Send money to a beneficiary
  - name: Payins
    description: Collect money into your books
  - name: Swaps
    description: FX between your own books
  - name: Events
    description: Durable event log for webhook polling
paths:
  /me:
    get:
      operationId: getMe
      tags: [Account]
      summary: Me
      description: |
        Returns the workspace and environment bound to the API key.
      responses:
        "200":
          description: Key context
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
              examples:
                sandbox:
                  value:
                    object: me
                    tenant_id: "ten_01EXAMPLE"
                    environment: sandbox
                    key:
                      id: "key_01EXAMPLE"
                      prefix: "sk_test_19d0"
                      expires_at: "2026-12-31T00:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /events:
    get:
      operationId: listEvents
      tags: [Events]
      summary: Events
      description: |
        Returns durable events for the environment bound to the API key
        (newest first). Use `ending_before` (unix seconds) to page older.
      parameters:
        - name: limit
          in: query
          description: Max events to return (1–100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: ending_before
          in: query
          description: Unix timestamp; return events strictly older than this.
          schema:
            type: integer
      responses:
        "200":
          description: Event list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [list]
                    description: Object type discriminator for list responses.
                  data:
                    type: array
                    description: Events newest first.
                    items:
                      $ref: "#/components/schemas/Event"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /events/{id}:
    get:
      operationId: getEvent
      tags: [Events]
      summary: Event
      description: |
        Returns one durable event by id for the environment bound to the API
        key. Use this to fetch a missed delivery when you already have the id
        (from a webhook body or a prior list poll).
      parameters:
        - name: id
          in: path
          required: true
          description: Event id (UUID).
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Event
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Event"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Forbidden"
  /ping:
    post:
      operationId: ping
      tags: [Events]
      summary: Ping
      description: |
        Persists `event.ping` and queues a delivery when the env webhook is
        active and subscribed to that type.
      responses:
        "200":
          description: Queued event
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Event"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /balances:
    get:
      operationId: listBalances
      tags: [Balances]
      summary: Balances
      responses:
        "200":
          description: Balance list
        "401":
          $ref: "#/components/responses/Unauthorized"
  /transactions:
    get:
      operationId: listTransactions
      tags: [Transactions]
      summary: Transactions
      description: |
        List activity. Each row includes `counterparty` (with `id` when the
        recipient is a saved beneficiary) and `quote.id` when a firm FX quote
        was used. Amounts are Ledger minor units with `scale`.
      parameters:
        - name: limit
          in: query
          description: Page size (default 25).
          schema: { type: integer, default: 25 }
        - name: page
          in: query
          description: 1-based page index.
          schema: { type: integer, default: 1 }
        - name: kind
          in: query
          description: "Filter by kind (`payout`, `inflow`, `swap`, …)."
          schema: { type: string }
        - name: status
          in: query
          description: "Filter by status (`completed`, `failed`, `processing`, …)."
          schema: { type: string }
        - name: currency
          in: query
          description: Filter by book currency (ISO 4217 / stablecoin).
          schema: { type: string }
      responses:
        "200":
          description: Transaction list
        "401":
          $ref: "#/components/responses/Unauthorized"
  /transactions/{id}:
    get:
      operationId: getTransaction
      tags: [Transactions]
      summary: Transaction
      description: |
        Full transaction: timeline, source (`payout` / `inflow` / `swap`),
        expanded `counterparty` (beneficiary or sender fields), and `quote`
        (id, rate, currencies, amounts) when FX was locked.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Transaction with timeline and source detail
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Forbidden"
  /beneficiaries:
    get:
      operationId: listBeneficiaries
      tags: [Beneficiaries]
      summary: Beneficiaries
      responses:
        "200":
          description: Beneficiary list
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createBeneficiary
      tags: [Beneficiaries]
      summary: Create Beneficiary
      description: |
        Save a payout recipient. Scheme and routing keys must match a corridor
        from `GET /corridors`. Emits `event.beneficiary.created`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBeneficiaryRequest"
            examples:
              ngn_bank:
                summary: Nigerian bank account (person)
                value:
                  kind: person
                  first_name: Ada
                  last_name: Okoye
                  currency: NGN
                  country: NG
                  scheme: ng_nip
                  holder_name: Ada Okoye
                  routing:
                    bank_code: "058"
                    account_number: "0123456789"
      responses:
        "200":
          description: Created beneficiary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Beneficiary"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /beneficiaries/{id}:
    get:
      operationId: getBeneficiary
      tags: [Beneficiaries]
      summary: Beneficiary
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Beneficiary
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      operationId: updateBeneficiary
      tags: [Beneficiaries]
      summary: Update Beneficiary
      description: |
        Update mutable fields only (nickname, email, relationship, address).
        Routing and scheme cannot change — create a new beneficiary instead.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
          description: Beneficiary id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateBeneficiaryRequest"
            examples:
              nickname:
                value:
                  nickname: Ada (supplier)
                  email: ada@example.com
      responses:
        "200":
          description: Updated beneficiary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Beneficiary"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /beneficiaries/{id}/archive:
    post:
      operationId: archiveBeneficiary
      tags: [Beneficiaries]
      summary: Archive Beneficiary
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Archived beneficiary
        "401":
          $ref: "#/components/responses/Unauthorized"
  /corridors:
    get:
      operationId: listCorridors
      tags: [Catalog]
      summary: Corridors
      responses:
        "200":
          description: Corridor catalog
        "401":
          $ref: "#/components/responses/Unauthorized"
  /fx/rate:
    get:
      operationId: getFxRate
      tags: [FX]
      summary: FX Rate
      parameters:
        - name: from
          in: query
          required: true
          description: Source currency code.
          schema: { type: string, example: USD }
        - name: to
          in: query
          required: true
          description: Destination currency code.
          schema: { type: string, example: NGN }
      responses:
        "200":
          description: Rate
        "401":
          $ref: "#/components/responses/Unauthorized"
  /fx/quotes:
    post:
      operationId: createFxQuote
      tags: [FX]
      summary: FX Quote
      description: |
        Lock a firm rate. Use `purpose: swap` for internal transfers between
        your books, then pass the returned `id` as `quote_id` to `POST /swaps`.
        Omit purpose (or leave empty) for a payout quote.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from, to]
              properties:
                from:
                  type: string
                  description: Source book currency (ISO 4217 or stablecoin code).
                  example: USD
                to:
                  type: string
                  description: Destination book currency.
                  example: NGN
                amount_from:
                  type: integer
                  description: |
                    Source amount in Ledger minor units (cents/kobo). Set exactly
                    one of `amount_from` or `amount_to`.
                  example: 10000
                amount_to:
                  type: integer
                  description: "Destination amount in Ledger minor units. Set exactly one of `amount_from` or `amount_to`."
                purpose:
                  type: string
                  enum: [swap]
                  description: |
                    Omit for a payout quote. Set to `swap` when the quote will be
                    passed to `POST /swaps`.
            examples:
              swap:
                summary: Quote for an internal transfer
                value:
                  from: USD
                  to: NGN
                  amount_from: 10000
                  purpose: swap
              payout:
                summary: Quote for a cross-currency payout
                value:
                  from: USD
                  to: NGN
                  amount_from: 10000
      responses:
        "200":
          description: Firm quote
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    enum: [fx_quote]
                    description: Object type discriminator.
                  id:
                    type: string
                    format: uuid
                    description: "Firm quote id — pass as `quote_id` to payouts or swaps."
                  from:
                    type: string
                    description: Source currency.
                  to:
                    type: string
                    description: Destination currency.
                  amount_from:
                    type: integer
                    description: Debit amount in Ledger minor units.
                  amount_to:
                    type: integer
                    description: Credit amount in Ledger minor units at the locked rate.
                  scale_from:
                    type: integer
                    description: "Decimal places for `from` (usually 2)."
                  scale_to:
                    type: integer
                    description: "Decimal places for `to`."
                  rate:
                    type: string
                    description: "Locked mid-market-style rate as a decimal string (`to` per 1 `from`)."
                  expires_at:
                    type: string
                    format: date-time
                    description: When this quote can no longer be used.
                  valid_for_seconds:
                    type: integer
                    description: Seconds remaining at creation time.
              examples:
                quote:
                  value:
                    object: fx_quote
                    id: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
                    from: USD
                    to: NGN
                    amount_from: 10000
                    amount_to: 1550000
                    scale_from: 2
                    scale_to: 2
                    rate: "155.00"
                    expires_at: "2026-10-27T13:02:00Z"
                    valid_for_seconds: 90
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payouts:
    post:
      operationId: createPayout
      tags: [Payouts]
      summary: Create Payout
      description: |
        Create and authorise a send in one step. The API key replaces the
        workspace PIN. Requires `Idempotency-Key`. Cross-currency sends need
        `quote_id` from `POST /fx/quotes`. If maker-checker applies, status is
        `pending_approval` until a second admin approves in the dashboard.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: |
            Client-generated key so retries do not create duplicate payouts.
            Reuse the same key for the same logical send.
          schema:
            type: string
            minLength: 8
            maxLength: 100
          example: payout-2026-10-27-001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePayoutRequest"
            examples:
              same_currency:
                summary: Same-currency send (NGN → NGN)
                value:
                  beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                  currency: NGN
                  amount: 5000000
                  purpose: supplier_goods
                  invoice_ref: INV-2026-042
                  note: October supplier invoice
              cross_currency:
                summary: Cross-currency (needs quote_id)
                value:
                  beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                  currency: USD
                  amount: 10000
                  purpose: services
                  invoice_ref: INV-2026-042
                  quote_id: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
      responses:
        "200":
          description: "Payout (queued, pending_approval, or rejected)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
              examples:
                queued:
                  value:
                    object: payout
                    id: "6e0e15cf-8736-44fc-bb9b-372eb7b14145"
                    status: queued
                    currency: NGN
                    amount: 5000000
                    fee: 15000
                    transfer_fee: 10000
                    scheme: ngn_bank
                    country: NG
                    stan: OUT-2610-ABCDEF12
                    beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                    purpose: supplier_goods
                    purpose_label: Supplier payment for goods
                    note: October supplier invoice
                    invoice_ref: INV-2026-042
                    cancellable: false
                    created_at: "2026-10-27T13:00:00Z"
                    updated_at: "2026-10-27T13:00:01Z"
                    authorised_at: "2026-10-27T13:00:01Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payouts/fee:
    post:
      operationId: payoutFee
      tags: [Payouts]
      summary: Payout Fee
      description: Estimate transfer fee before creating a payout.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayoutFeeRequest"
            examples:
              estimate:
                summary: Fee for a NGN send
                value:
                  beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                  currency: NGN
                  amount: 5000000
      responses:
        "200":
          description: Fee quote
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayoutFee"
              examples:
                fee:
                  value:
                    object: payout_fee
                    currency: NGN
                    amount: 5000000
                    transfer_fee: 10000
                    total: 5000000
                    exact: false
                    scale: 2
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payouts/purposes:
    get:
      operationId: payoutPurposes
      tags: [Payouts]
      summary: Purposes
      description: Allowed purpose codes (and document rules) for this workspace.
      responses:
        "200":
          description: Purpose rules
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              examples:
                business:
                  value:
                    purposes:
                      - code: supplier_goods
                        label: Supplier payment for goods
                      - code: services
                        label: Payment for services
                      - code: professional_fees
                        label: Professional fees
                      - code: salaries
                        label: Salaries and payroll
                      - code: rent
                        label: Rent
                      - code: other
                        label: Other
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payouts/{id}:
    get:
      operationId: getPayout
      tags: [Payouts]
      summary: Get Payout
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
          example: "6e0e15cf-8736-44fc-bb9b-372eb7b14145"
      responses:
        "200":
          description: Payout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
              examples:
                queued:
                  value:
                    object: payout
                    id: "6e0e15cf-8736-44fc-bb9b-372eb7b14145"
                    status: queued
                    currency: NGN
                    amount: 5000000
                    fee: 15000
                    transfer_fee: 10000
                    scheme: ngn_bank
                    country: NG
                    stan: OUT-2610-ABCDEF12
                    beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                    purpose: supplier_goods
                    purpose_label: Supplier payment for goods
                    invoice_ref: INV-2026-042
                    cancellable: false
                    created_at: "2026-10-27T13:00:00Z"
                    updated_at: "2026-10-27T13:00:01Z"
                    authorised_at: "2026-10-27T13:00:01Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Forbidden"
  /payouts/{id}/cancel:
    post:
      operationId: cancelPayout
      tags: [Payouts]
      summary: Cancel Payout
      description: Cancel while awaiting auth or pending approval. Queued sends cannot be cancelled via API.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
          example: "6e0e15cf-8736-44fc-bb9b-372eb7b14145"
      responses:
        "200":
          description: Cancelled payout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
              examples:
                cancelled:
                  value:
                    object: payout
                    id: "6e0e15cf-8736-44fc-bb9b-372eb7b14145"
                    status: cancelled
                    currency: NGN
                    amount: 5000000
                    fee: 0
                    transfer_fee: 0
                    scheme: ngn_bank
                    country: NG
                    stan: OUT-2610-ABCDEF12
                    beneficiary_id: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
                    cancellable: false
                    created_at: "2026-10-27T13:00:00Z"
                    updated_at: "2026-10-27T13:05:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /swaps:
    post:
      operationId: createSwap
      tags: [Swaps]
      summary: Create Swap
      description: |
        Move funds between your own books using a firm quote from
        `POST /fx/quotes` with `purpose: swap`. Requires `Idempotency-Key`.
        The API key replaces the workspace PIN.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Client-generated key so retries do not create duplicate swaps.
          schema: { type: string, minLength: 1, maxLength: 128 }
          example: swap-2026-10-27-001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quote_id]
              properties:
                quote_id:
                  type: string
                  format: uuid
                  description: |
                    Id from `POST /fx/quotes` with `purpose: swap`. Must still be
                    unexpired and unused.
                  example: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
            examples:
              swap:
                value:
                  quote_id: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
      responses:
        "200":
          description: "Swap (processing, completed, or failed)"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Swap"
              examples:
                processing:
                  value:
                    object: swap
                    id: "14d2759d-c6b1-5e5f-85b2-af84bc650f57"
                    status: processing
                    origin: keette
                    from: USD
                    to: NGN
                    amount_from: 10000
                    amount_to: 1550000
                    scale_from: 2
                    scale_to: 2
                    fx_rate: "155.00"
                    quote_id: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
                    stan: SWP-2610-ABCDEF12
                    created_at: "2026-10-27T13:00:00Z"
                    updated_at: "2026-10-27T13:00:01Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /swaps/{id}:
    get:
      operationId: getSwap
      tags: [Swaps]
      summary: Get Swap
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
          example: "14d2759d-c6b1-5e5f-85b2-af84bc650f57"
      responses:
        "200":
          description: Swap
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Swap"
              examples:
                completed:
                  value:
                    object: swap
                    id: "14d2759d-c6b1-5e5f-85b2-af84bc650f57"
                    status: completed
                    origin: keette
                    from: USD
                    to: NGN
                    amount_from: 10000
                    amount_to: 1550000
                    scale_from: 2
                    scale_to: 2
                    fx_rate: "155.00"
                    quote_id: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
                    stan: SWP-2610-ABCDEF12
                    created_at: "2026-10-27T13:00:00Z"
                    updated_at: "2026-10-27T13:00:05Z"
                    completed_at: "2026-10-27T13:00:05Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Forbidden"
  /payins/sources:
    get:
      operationId: listPayinSources
      tags: [Payins]
      summary: Sources
      description: |
        Funding options into a target wallet (bank details, momo, Interac, crypto).
      parameters:
        - name: target_currency
          in: query
          required: true
          description: Book currency you want to credit (ISO 4217 / stablecoin).
          schema: { type: string, example: NGN }
      responses:
        "200":
          description: Source catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayinSources"
              examples:
                ngn:
                  value:
                    object: payin_sources
                    target_currency: NGN
                    sources:
                      - kind: bank
                        currency: NGN
                        modes:
                          - mode: bank_details
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payins/fee:
    post:
      operationId: payinFee
      tags: [Payins]
      summary: Payin Fee
      description: Estimate partner + Keette fee before starting a deposit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayinFeeRequest"
            examples:
              bank:
                value:
                  method: bank_transfer
                  target_currency: NGN
                  amount: 5000000
      responses:
        "200":
          description: Fee quote
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayinFee"
              examples:
                fee:
                  value:
                    object: payin_fee
                    currency: NGN
                    amount: 5000000
                    partner_fee: 0
                    margin: 5000
                    fee: 5000
                    credit_amount: 4995000
                    scale: 2
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payins/preview:
    post:
      operationId: payinPreview
      tags: [Payins]
      summary: Payin Preview
      description: |
        Fee + indicative FX before `POST /payins/prepare`. Use the same `mode`
        and currencies you will send to prepare.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayinPreviewRequest"
            examples:
              bank_details:
                value:
                  target_currency: NGN
                  mode: bank_details
                  amount: 5000000
      responses:
        "200":
          description: Preview
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayinPreview"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payins/prepare:
    post:
      operationId: preparePayin
      tags: [Payins]
      summary: Prepare Payin
      description: |
        Open a deposit intent and return receive instructions (bank details,
        crypto address, momo prompt, or Interac). Requires `Idempotency-Key`.
        Modes: `bank_details`, `crypto_address`, `momo`, `interac`.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Client-generated key so retries return the same intent.
          schema: { type: string, minLength: 1, maxLength: 64 }
          example: payin-prep-2026-10-27-001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PreparePayinRequest"
            examples:
              bank_details:
                summary: NGN bank deposit instructions
                value:
                  target_currency: NGN
                  mode: bank_details
                  amount: 5000000
              crypto:
                summary: USDC on Base
                value:
                  target_currency: USDC
                  source_currency: USDC
                  mode: crypto_address
                  amount: 10000000
                  network: base
      responses:
        "200":
          description: Deposit intent with instructions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payin"
              examples:
                bank:
                  value:
                    object: payin
                    id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    status: pending
                    method: bank_transfer
                    target_currency: NGN
                    amount: 5000000
                    collection_mode: bank_details
                    stan: IN-2610-ABCDEF12
                    instructions:
                      account_name: Keette Collections
                      account_number: "0123456789"
                      bank_name: Guaranty Trust Bank
                      bank_code: "058"
                    created_at: "2026-10-27T13:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payins:
    post:
      operationId: createPayin
      tags: [Payins]
      summary: Create Payin
      description: |
        Start a partner funding session (`bank_transfer`, momo, Interac,
        Apple Pay, crypto checkout). Prefer `POST /payins/prepare` for static
        bank / crypto address instructions. Requires `Idempotency-Key`.
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Client-generated key so retries return the same attempt.
          schema: { type: string, minLength: 1, maxLength: 64 }
          example: payin-2026-10-27-001
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePayinRequest"
            examples:
              bank_transfer:
                value:
                  method: bank_transfer
                  target_currency: GBP
                  amount: 10000
                  redirect_url: https://merchant.example/payins/return
      responses:
        "200":
          description: Funding attempt (may include checkout_url)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payin"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /payins/{id}:
    get:
      operationId: getPayin
      tags: [Payins]
      summary: Get Payin
      parameters:
        - name: id
          in: path
          required: true
          description: Payin attempt id.
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: Payin
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payin"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: |
        Secret API key from **Settings → Developers → API keys**.
        Example: `Authorization: Bearer sk_test_…`
  responses:
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              type: authentication_error
              code: invalid_api_key
              message: Invalid API key provided.
    Forbidden:
      description: API disabled for this tenant or globally
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              type: permission_error
              code: api_disabled
              message: Public API access is disabled for this workspace.
  schemas:
    Me:
      type: object
      required: [object, tenant_id, environment, key]
      properties:
        object:
          type: string
          enum: [me]
          description: Object type discriminator.
        tenant_id:
          type: string
          description: Workspace id bound to this API key.
        environment:
          type: string
          enum: [live, sandbox]
          description: "Environment selected by the key (`sk_live_…` vs `sk_test_…`)."
        key:
          type: object
          description: Non-secret metadata for the authenticating key.
          required: [id, prefix, expires_at]
          properties:
            id:
              type: string
              description: Internal key id.
            prefix:
              type: string
              description: Non-secret key prefix shown in the dashboard.
            expires_at:
              type: string
              format: date-time
              description: When this key stops working (UTC).
    Event:
      type: object
      required: [id, object, type, created, livemode, data]
      properties:
        id:
          type: string
          format: uuid
          description: Event id (also used for webhook delivery tracking).
        object:
          type: string
          enum: [event]
          description: Object type discriminator.
        type:
          type: string
          description: "Event type string (e.g. `event.ping`, `event.payout.completed`)."
          example: event.ping
        created:
          type: integer
          description: Unix seconds when the event was persisted.
        livemode:
          type: boolean
          description: "`true` when emitted in the live environment."
        environment:
          type: string
          enum: [live, sandbox]
          description: Environment that produced the event.
        data:
          type: object
          additionalProperties: true
          description: "Event-specific payload (shape depends on `type`)."
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [type, code, message]
          properties:
            type:
              type: string
              description: Broad error class for client handling.
              enum:
                - invalid_request
                - authentication_error
                - permission_error
                - rate_limit
                - api_error
            code:
              type: string
              description: "Stable machine-readable code (e.g. `parameter_invalid`)."
            message:
              type: string
              description: Human-readable explanation safe to show developers.
            param:
              type: string
              description: "Request field that failed validation, when applicable."
            doc_url:
              type: string
              format: uri
              description: Optional link to docs for this error.
    CreateBeneficiaryRequest:
      type: object
      required: [kind, currency, scheme, holder_name, routing]
      properties:
        kind:
          type: string
          enum: [person, business]
          description: "Recipient type. Use `person` with first/last name, or `business` with `business_name`."
          example: person
        first_name:
          type: string
          description: "Given name (required when `kind` is `person`)."
          example: Ada
        last_name:
          type: string
          description: "Family name (required when `kind` is `person`)."
          example: Okoye
        business_name:
          type: string
          description: "Legal business name (required when `kind` is `business`)."
        nickname:
          type: string
          description: Optional label shown in your dashboard and lists.
        email:
          type: string
          format: email
          description: Optional contact email for the recipient.
        relationship:
          type: string
          description: |
            Relationship code from `GET /corridors` → `relationships` (e.g. supplier,
            employee). Required for some personal corridors.
        address:
          $ref: "#/components/schemas/Address"
        currency:
          type: string
          description: Payout currency the recipient receives (must match the scheme).
          example: NGN
        country:
          type: string
          description: "ISO 3166-1 alpha-2 country for the corridor (e.g. `NG`)."
          example: NG
        scheme:
          type: string
          description: |
            Corridor scheme id from `GET /corridors` (e.g. `ng_nip`, `ke_mpesa`,
            `eu_sepa`, `us_ach`). Determines required `routing` keys.
          example: ng_nip
        holder_name:
          type: string
          description: Account holder name as registered with the receiving bank or wallet.
          example: Ada Okoye
        routing:
          type: object
          additionalProperties: true
          description: |
            Scheme-specific account identifiers. Keys depend on the corridor
            (e.g. `bank_code` + `account_number` for `ng_nip`). See corridor
            field definitions from `GET /corridors`.
          example:
            bank_code: "058"
            account_number: "0123456789"
    UpdateBeneficiaryRequest:
      type: object
      properties:
        nickname:
          type: string
          description: Display label for lists and the dashboard.
        email:
          type: string
          format: email
          description: Contact email.
        relationship:
          type: string
          description: Relationship code (see corridors catalog).
        address:
          $ref: "#/components/schemas/Address"
    Address:
      type: object
      description: Postal address for the recipient when the corridor requires it.
      properties:
        line_1:
          type: string
          description: Street address line 1.
        line_2:
          type: string
          description: Street address line 2.
        city:
          type: string
          description: City or town.
        state:
          type: string
          description: "State, province, or region."
        postal_code:
          type: string
          description: Postal or ZIP code.
        country:
          type: string
          description: ISO 3166-1 alpha-2 country code.
    Beneficiary:
      type: object
      properties:
        object:
          type: string
          enum: [beneficiary]
          description: Object type discriminator.
        id:
          type: string
          format: uuid
          description: "Beneficiary id — pass as `beneficiary_id` on payouts."
        kind:
          type: string
          enum: [person, business]
          description: Recipient type.
        name:
          type: string
          description: Display name derived from person or business fields.
        first_name:
          type: string
          description: "Given name when `kind` is `person`."
        last_name:
          type: string
          description: "Family name when `kind` is `person`."
        business_name:
          type: string
          description: "Legal name when `kind` is `business`."
        nickname:
          type: string
          description: Optional display label.
        email:
          type: string
          description: Contact email.
        relationship:
          type: string
          description: Relationship to your workspace.
        address:
          $ref: "#/components/schemas/Address"
        currency:
          type: string
          description: Currency the recipient is paid in.
        country:
          type: string
          description: Corridor country.
        method:
          type: string
          description: "High-level rail family (`bank`, `mobile_money`, …)."
        scheme:
          type: string
          description: Corridor scheme id.
        scheme_label:
          type: string
          description: Human-readable scheme name.
        settlement:
          type: string
          description: "Typical settlement timing (e.g. Instant, Same day)."
        holder_name:
          type: string
          description: Account holder name on the rail.
        routing:
          type: object
          additionalProperties: true
          description: Scheme-specific account identifiers (may be partially masked).
        route_label:
          type: string
          description: Short label for the destination account.
        status:
          type: string
          description: "Lifecycle status (`active`, `archived`, …)."
        missing_fields:
          type: array
          items: { type: string }
          description: Fields still required before the beneficiary can be paid.
        created_at:
          type: string
          format: date-time
          description: When the beneficiary was created (UTC).
        updated_at:
          type: string
          format: date-time
          description: When the beneficiary was last updated (UTC).
        archived_at:
          type: string
          format: date-time
          description: "When archived, if applicable."
        livemode:
          type: boolean
          description: "`true` when created with a live key."
        environment:
          type: string
          enum: [live, sandbox]
          description: Environment of the creating API key.
    CreatePayoutRequest:
      type: object
      required: [beneficiary_id, currency, amount]
      properties:
        beneficiary_id:
          type: string
          format: uuid
          description: "Id of a saved beneficiary to pay (`GET /beneficiaries`)."
          example: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
        currency:
          type: string
          description: |
            Source book currency to debit (ISO 4217 or stablecoin). May differ
            from the beneficiary currency when `quote_id` is supplied.
          example: NGN
        amount:
          type: integer
          description: "Gross send amount in Ledger minor units (e.g. kobo, cents)."
          example: 5000000
        purpose:
          type: string
          description: |
            Purpose code from `GET /payouts/purposes`. Business workspaces cannot
            use `family_support` (personal only). Common business codes:
            `supplier_goods`, `services`, `professional_fees`, `salaries`,
            `rent`, `other` (requires `note`).
          example: supplier_goods
        note:
          type: string
          description: "Free-text note. Required when `purpose` is `other`."
          example: October supplier invoice
        invoice_ref:
          type: string
          description: Optional invoice or remittance reference (business accounts).
          example: INV-2026-042
        quote_id:
          type: string
          format: uuid
          description: |
            Firm FX quote id from `POST /fx/quotes` (omit `purpose`, or leave empty).
            Required when the beneficiary currency differs from `currency`.
          example: "e6bd10ee-6e53-4aaa-9cd0-a2bb980cf73e"
    PayoutFeeRequest:
      type: object
      required: [beneficiary_id, currency, amount]
      properties:
        beneficiary_id:
          type: string
          format: uuid
          description: Beneficiary the fee estimate applies to.
          example: "bbd938a2-dbbe-43bf-bfb9-1c5dbe444523"
        currency:
          type: string
          description: Source book currency for the planned send.
          example: NGN
        amount:
          type: integer
          description: Planned send amount in Ledger minor units.
          example: 5000000
        quote_id:
          type: string
          format: uuid
          description: Optional firm quote id for cross-currency fee accuracy.
        recipient_amount:
          type: integer
          description: |
            Optional estimated recipient amount in their currency minor units when
            you do not yet have a firm `quote_id`.
    PayoutFee:
      type: object
      properties:
        object:
          type: string
          enum: [payout_fee]
          description: Object type discriminator.
        currency:
          type: string
          description: "Currency of `amount`, `transfer_fee`, and `total`."
        amount:
          type: integer
          description: Requested send amount in Ledger minor units.
        transfer_fee:
          type: integer
          description: Estimated customer-visible transfer fee in minor units.
        total:
          type: integer
          description: "Amount plus fee (what leaves the book), in minor units."
        exact:
          type: boolean
          description: |
            `true` when the fee is final for this input; `false` when indicative
            (e.g. before a firm FX quote).
        min_payment:
          type: integer
          description: "Minimum allowed send for this corridor, if applicable."
        scale:
          type: integer
          description: Decimal places for the currency (usually 2).
    Swap:
      type: object
      required: [object, id, status, from, to, amount_from, amount_to, stan]
      properties:
        object:
          type: string
          enum: [swap]
          description: Object type discriminator.
        id:
          type: string
          format: uuid
          description: Swap id.
        status:
          type: string
          enum: [processing, completed, failed, reversed]
          description: Lifecycle status of the book-to-book transfer.
        origin:
          type: string
          description: "How the swap was initiated (`keette` for API/dashboard)."
        from:
          type: string
          description: Debited book currency.
        to:
          type: string
          description: Credited book currency.
        amount_from:
          type: integer
          description: Debit amount in Ledger minor units.
        amount_to:
          type: integer
          description: Credit amount in Ledger minor units.
        scale_from:
          type: integer
          description: "Decimal places for `from`."
        scale_to:
          type: integer
          description: "Decimal places for `to`."
        fx_rate:
          type: string
          description: Applied rate as a decimal string.
        quote_id:
          type: string
          format: uuid
          description: Firm quote that locked the rate.
        reason:
          type: string
          description: Failure or reversal reason when applicable.
        stan:
          type: string
          description: System trace / reference shown in statements.
        created_at:
          type: string
          format: date-time
          description: Creation time (UTC).
        updated_at:
          type: string
          format: date-time
          description: Last status change (UTC).
        completed_at:
          type: string
          format: date-time
          description: "When the swap finished successfully, if completed."
    Payout:
      type: object
      required: [object, id, status, currency, amount, stan, beneficiary_id]
      properties:
        object:
          type: string
          enum: [payout]
          description: Object type discriminator.
        id:
          type: string
          format: uuid
          description: Payout id.
        status:
          type: string
          enum:
            - awaiting_auth
            - pending_approval
            - queued
            - sending
            - completed
            - failed
            - cancelled
            - rejected
            - expired
          description: |
            Lifecycle status. `pending_approval` means maker-checker is waiting
            on a second admin in the dashboard. `queued`/`sending` are in flight.
        currency:
          type: string
          description: Source book currency that was (or will be) debited.
        amount:
          type: integer
          description: Gross send in Ledger minor units.
        fee:
          type: integer
          description: Customer-visible transfer fee (partner + margin + cross-border).
        transfer_fee:
          type: integer
          description: "Transfer fee component in minor units (same as `fee` for most corridors)."
        scheme:
          type: string
          description: Corridor scheme used for the send.
        country:
          type: string
          description: Destination country (ISO 3166-1 alpha-2).
        stan:
          type: string
          description: System trace / remittance reference.
        beneficiary_id:
          type: string
          format: uuid
          description: Recipient beneficiary id.
        purpose:
          type: string
          description: Purpose code submitted with the payout.
        purpose_label:
          type: string
          description: Human-readable purpose label.
        note:
          type: string
          description: Free-text note from the request.
        invoice_ref:
          type: string
          description: Invoice or remittance reference.
        quote_id:
          type: string
          format: uuid
          description: Firm FX quote used when currencies differed.
        fx_rate:
          type: string
          description: Applied FX rate when cross-currency.
        recipient_currency:
          type: string
          description: Currency credited to the beneficiary.
        recipient_amount:
          type: integer
          description: "Amount the beneficiary receives, in their minor units."
        recipient_scale:
          type: integer
          description: "Decimal places for `recipient_currency`."
        cancellable:
          type: boolean
          description: "Whether `POST /payouts/{id}/cancel` can still succeed."
        failure_reason:
          type: string
          description: "Partner or validation failure message when status is `failed`/`rejected`."
        authorised_at:
          type: string
          format: date-time
          description: When the send was authorised (API key or PIN).
        completed_at:
          type: string
          format: date-time
          description: When the payout reached a terminal success state.
        created_at:
          type: string
          format: date-time
          description: Creation time (UTC).
        updated_at:
          type: string
          format: date-time
          description: Last status change (UTC).
    PayinFeeRequest:
      type: object
      required: [target_currency, amount]
      properties:
        method:
          type: string
          description: |
            Funding method. Accepted values: `bank_transfer`, `momo`, `interac`,
            `crypto`, `apple_pay`. `bank_details` / `bank` are aliases for
            `bank_transfer`. Provide `method` or `mode`.
          example: bank_transfer
        mode:
          type: string
          description: |
            Alternative to `method` using prepare modes (`bank_details`,
            `crypto_address`, `momo`, `interac`). Mapped to the matching method
            for pricing.
          example: bank_details
        target_currency:
          type: string
          description: Book currency that will be credited.
          example: NGN
        amount:
          type: integer
          description: Funding amount in Ledger minor units of `currency` (or target when omitted).
          example: 5000000
        currency:
          type: string
          description: Currency of `amount` for the fee quote; defaults to `target_currency`.
    PayinFee:
      type: object
      properties:
        object:
          type: string
          enum: [payin_fee]
          description: Object type discriminator.
        currency:
          type: string
          description: Currency of the quoted amounts.
        amount:
          type: integer
          description: Requested funding amount in minor units.
        partner_fee:
          type: integer
          description: Partner rail cost in minor units.
        margin:
          type: integer
          description: Keette concession / margin in minor units.
        fee:
          type: integer
          description: Total fee charged to the payer (`partner_fee` + `margin`).
        credit_amount:
          type: integer
          description: Net amount expected to credit the book after fees.
        margin_source:
          type: string
          description: Pricing source for the margin line.
        product:
          type: string
          description: Internal pricing product key.
        scale:
          type: integer
          description: Decimal places for the currency.
    PayinPreviewRequest:
      type: object
      required: [target_currency, mode, amount]
      properties:
        target_currency:
          type: string
          description: Book currency to credit.
          example: NGN
        source_currency:
          type: string
          description: Currency the payer funds in; defaults to `target_currency`.
        mode:
          type: string
          enum: [bank_details, crypto_address, momo, interac]
          description: Collection mode (same values as prepare).
          example: bank_details
        amount:
          type: integer
          description: What the payer sends in `source_currency` minor units.
          example: 5000000
        channel:
          type: string
          description: Momo channel id when `mode` is `momo` (e.g. `KE-MPESA`).
        network:
          type: string
          description: Crypto network when `mode` is `crypto_address` (e.g. `base`).
    PayinPreview:
      type: object
      properties:
        object:
          type: string
          enum: [payin_preview]
          description: Object type discriminator.
        target_currency:
          type: string
          description: Book currency that will be credited.
        source_currency:
          type: string
          description: Currency the payer funds in.
        mode:
          type: string
          description: Collection mode used for the estimate.
        method:
          type: string
          description: Start-compatible method string for `POST /payins`.
        pay_amount:
          type: integer
          description: Amount the payer should send (source minor units).
        fee:
          type: integer
          description: Total fee in `fee_currency` minor units.
        fee_currency:
          type: string
          description: Currency of the fee line.
        partner_fee:
          type: integer
          description: Partner portion of the fee.
        margin:
          type: integer
          description: Keette margin portion of the fee.
        credit_amount:
          type: integer
          description: Expected net credit to the target book.
        target_gross:
          type: integer
          description: Target-side amount before fee (after FX when cross-currency).
        rate:
          type: string
          description: Indicative FX rate when currencies differ.
        rate_as_of:
          type: string
          format: date-time
          description: When the indicative rate was observed.
        indicative:
          type: boolean
          description: "`true` when FX is not a firm locked quote."
        product:
          type: string
          description: Pricing product key.
        channel:
          type: string
          description: Echo of momo channel when set.
        network:
          type: string
          description: Echo of crypto network when set.
        scale_source:
          type: integer
          description: Decimal places for `source_currency`.
        scale_target:
          type: integer
          description: Decimal places for `target_currency`.
    PreparePayinRequest:
      type: object
      required: [target_currency, mode, amount]
      properties:
        target_currency:
          type: string
          description: Book currency to credit.
          example: NGN
        source_currency:
          type: string
          description: |
            Currency of the receive rail. Defaults to `target_currency`. For
            Interac defaults to CAD; momo may infer KES/TZS from `channel`.
        mode:
          type: string
          enum: [bank_details, crypto_address, momo, interac]
          description: How funds will arrive.
          example: bank_details
        amount:
          type: integer
          description: Amount the payer sends in `source_currency` minor units.
          example: 5000000
        target_amount:
          type: integer
          description: Required when `source_currency` differs from `target_currency` — expected credit in target minor units.
        network:
          type: string
          description: Crypto network (`base`, `ethereum`, …). Defaults to `base` for crypto.
        narration:
          type: string
          description: Optional payment reference / memo.
        redirect_url:
          type: string
          format: uri
          description: Required for Interac — where the payer returns after approval.
        country_code:
          type: string
          description: ISO country hint for some rails.
        msisdn:
          type: string
          description: Mobile number for momo (E.164 or local as required by channel).
        channel:
          type: string
          description: Momo channel id (e.g. `KE-MPESA`).
        payer_name:
          type: string
          description: Optional payer display name.
        payer_email:
          type: string
          format: email
          description: Optional payer email.
    CreatePayinRequest:
      type: object
      required: [method, target_currency, amount]
      properties:
        method:
          type: string
          description: "Funding method (`bank_transfer`, `momo`, `interac`, `crypto`, `apple_pay`, …)."
          example: bank_transfer
        target_currency:
          type: string
          description: Book currency to credit.
          example: GBP
        amount:
          type: integer
          description: Amount in Ledger minor units (target, unless rail fields set).
          example: 10000
        rail_currency:
          type: string
          description: Optional rail currency when the payer pays in a different currency.
        rail_amount:
          type: integer
          description: Amount in `rail_currency` minor units when set.
        redirect_url:
          type: string
          format: uri
          description: Return URL after hosted checkout (required for open banking / Interac / crypto checkout).
        country_code:
          type: string
          description: ISO country hint.
        narration:
          type: string
          description: Optional payment memo.
        msisdn:
          type: string
          description: Mobile number for momo.
        channel:
          type: string
          description: Momo channel id.
        payer_name:
          type: string
          description: Optional payer name.
        payer_email:
          type: string
          format: email
          description: Optional payer email.
    PayinSources:
      type: object
      properties:
        object:
          type: string
          enum: [payin_sources]
          description: Object type discriminator.
        target_currency:
          type: string
          description: Book currency you queried.
        sources:
          type: array
          description: Available funding origins and modes.
          items:
            type: object
            properties:
              kind:
                type: string
                description: "Origin kind (`bank`, `momo`, `crypto`, …)."
              currency:
                type: string
                description: Source / rail currency for this origin.
              modes:
                type: array
                description: Collection modes under this origin.
                items:
                  type: object
                  properties:
                    mode:
                      type: string
                      description: Mode id for prepare (`bank_details`, …).
                    channels:
                      type: array
                      items: { type: string }
                      description: Momo channels when applicable.
                    networks:
                      type: array
                      items: { type: string }
                      description: Crypto networks when applicable.
    Payin:
      type: object
      properties:
        object:
          type: string
          enum: [payin]
          description: Object type discriminator.
        id:
          type: string
          format: uuid
          description: Payin attempt id.
        stan:
          type: string
          description: System trace / reference.
        method:
          type: string
          description: Funding method.
        target_currency:
          type: string
          description: Book currency to credit.
        rail_currency:
          type: string
          description: Currency on the payment rail when different.
        amount:
          type: integer
          description: Requested amount in target (or primary) minor units.
        rail_amount:
          type: integer
          description: Amount on the rail in rail minor units.
        target_amount:
          type: integer
          description: Expected credit to the target book in minor units.
        scale:
          type: integer
          description: Decimal places for `target_currency`.
        status:
          type: string
          description: "Lifecycle status (`pending`, `processing`, `completed`, `failed`, …)."
        checkout_url:
          type: string
          format: uri
          description: Hosted checkout URL when the method requires a redirect.
        redirect_url:
          type: string
          format: uri
          description: Return URL you supplied.
        source_kind:
          type: string
          description: Origin kind from prepare.
        collection_mode:
          type: string
          description: Prepare mode (`bank_details`, `crypto_address`, …).
        network:
          type: string
          description: Crypto network when applicable.
        instructions:
          type: object
          additionalProperties: true
          description: |
            Receive instructions for the payer (account number, bank name,
            wallet address, etc.). Shape depends on `collection_mode`.
        failure_reason:
          type: string
          description: Failure message when status is failed.
        created_at:
          type: string
          format: date-time
          description: Creation time (UTC).
        updated_at:
          type: string
          format: date-time
          description: Last status change (UTC).
        completed_at:
          type: string
          format: date-time
          description: When the deposit completed, if successful.
        livemode:
          type: boolean
          description: "`true` when created with a live key."
        environment:
          type: string
          enum: [live, sandbox]
          description: Environment of the creating API key.
