> ## Documentation Index
> Fetch the complete documentation index at: https://docs.panoflow.io/llms.txt
> Use this file to discover all available pages before exploring further.

# List transfers

> Newest first. Includes batch rows, scheduled runs, agent payments and reversals made by PanoFlow.

**Error codes:** `invalid_api_key`, `parameter_invalid`, `scope_missing`, `rate_limited`.



## OpenAPI

````yaml /api-reference/openapi.yaml get /transfers
openapi: 3.1.0
info:
  title: PanoFlow API
  version: 0.3.0
  description: >-
    Identity, business, team, customer-side onboarding (KYB), accounts,
    statements, the customer transactions view,

    in-app notifications and search for the PanoFlow public API. The customer
    web app (app.panoflow.io) uses exactly these endpoints;

    it has no private API. Rules: `CONVENTIONS.md`. Behaviour:
    `docs/architecture.md`. Design/architecture conflicts:

    `docs/discrepancies/identity-accounts.md`.


    ## Authentication


    * **Customer session** (`__Host-pf_session` cookie; the web app only). The
    browser calls the API through the web app's same-origin
      proxy, `https://app.panoflow.io/api/v1/...`, which forwards to `https://api.panoflow.io/v1/...` (ADR 0004). The cookie is host-only on
      app.panoflow.io (`__Host-` prefix, Secure, HttpOnly, SameSite=Lax, Path=/, no Domain), so the browser never sends it to
      api.panoflow.io and no credentialed CORS is needed; api.panoflow.io ignores a session cookie that didn't come through the proxy.
      The session ends after 15 minutes without activity or 12 hours after sign-in, whichever comes first, and is revoked on a password or
      2FA change (ADR 0005). A session starts `pending` and becomes `active` once sign-up is finished (legal name, phone verified,
      authenticator enrolled) or, for a returning user, once 2FA is passed.
    * **CSRF.** Every state-changing request (POST, PATCH, DELETE) made with the
    session cookie sends `X-CSRF-Token` with the
      `csrf_token` of the current session. The proxy passes the header and the browser's `Origin` through unchanged and never adds a
      token itself; the API checks the token against the session and that `Origin` is `https://app.panoflow.io`, and answers 403
      `csrf_token_invalid` otherwise. Sign-up, sign-in and the other unauthenticated auth calls have no token yet, so through the proxy
      only the `Origin` check applies to them. API keys and OAuth tokens never send the header.
    * **API key** (`Authorization: Bearer pk_live_…`) and **OAuth 2.1** access
    tokens (agents, MCP) can call the business, team-read,
      onboarding-read, account, statement, transaction and search operations allowed by their scopes. They can never call `/v1/auth/*`,
      `/v1/users/me/*` or `/v1/notifications*` (session only), and agent tokens never hold `team:*`.

    ## `x-required-scope`


    * `none`: unauthenticated (sign-up, sign-in, password reset, invite links).
    Rate-limited per IP and per account.

    * `self`: any signed-in user acting on their own user, session or
    notifications. Session only.

    * `<resource>:<read|write>`: the scope an API key or token needs. For
    sessions the scope comes from the member's role:


    | Role | Scopes (this file) | Notes |

    | --- | --- | --- |

    | owner | business:read/write, team:read/write, accounts:read/write |
    Everything, including closing the business account. |

    | admin | business:read/write, team:read/write, accounts:read/write |
    Everything except changing the owner. |

    | finance | business:read, team:read, accounts:read/write | Pays up to the
    approval threshold alone. |

    | approver | business:read, team:read, accounts:read | Decides pending
    approvals; can't start payments. |

    | developer | business:read, team:read | API keys, webhooks, logs. No
    balances or payments. |

    | viewer | business:read, team:read, accounts:read | Read only. |


    Some writes are limited further by role ("**Roles:**" in the operation
    description); a disallowed role gets 403 `role_not_permitted`.


    ## Step-up


    Operations marked "**Step-up**" need `X-Step-Up-Token` from `POST
    /v1/auth/step-up` (TOTP or backup code within 5 minutes).


    ## Errors


    Errors use the shared `Error` shape, with `error.response_code` (ISO 8583,
    CONVENTIONS §7a) mapped from `error.code`. Each operation lists the

    codes it can return. Non-money resources in this file carry no
    `response_code`; `Transaction` (a view of money movements) composes
    `ResponseOutcome`.



    Generated from panobaas-contracts; only operations marked `x-audience:
    public`.
  contact:
    name: PanoFlow API
    url: https://docs.panoflow.io
    email: api@panoflow.io
servers:
  - url: https://api.panoflow.io/v1
    description: Live and sandbox (sandbox is chosen by `pk_test_` keys and test sessions).
  - url: https://api-dev-9d8b.up.railway.app/v1
    description: >-
      Development (Railway). Synthetic data and fake partners; redeployed from
      main, may be reset at any time.
security: []
tags:
  - name: Business
    description: >-
      The business (`/v1/businesses/me`): CAC record, how it appears, and the
      approval threshold for the customer-side two-person rule.
    x-displayName: Business
  - name: Accounts
    description: >-
      Main, Payroll and sub-accounts with their balances; open, rename, close,
      share details, confirmation letters and balance totals. Static virtual
      accounts are in the collections API.
    x-displayName: Accounts
  - name: Statements
    description: >-
      Monthly and custom-range account statements, generated asynchronously as
      PDF and CSV.
    x-displayName: Statements
  - name: Transactions
    description: >-
      The customer transactions view: every movement in and out of the
      business's accounts, with filters, notes, attachments, receipts and CSV
      export.
    x-displayName: Transactions
  - name: Banks
    description: >
      The bank picker and bank name enquiry. Name enquiry asks the receiving
      bank for the name on an account before

      anyone pays it or saves it as a recipient.
    x-displayName: Banks
  - name: Recipients
    description: >
      The people and businesses a business pays (Payments → Recipients). Every
      recipient is name-enquiry verified when

      it is saved. Recipients are archived, never deleted.
    x-displayName: Recipients
  - name: Transfers
    description: >
      One resource for money leaving an account: `payout` to a bank account
      outside PanoFlow (NIP), and

      `own_account` moves between the business's own accounts in the same
      currency. `reversal` transfers made by

      PanoFlow also appear here, read-only.
    x-displayName: Transfers
  - name: Batches
    description: >
      Pay many: validate a CSV (or JSON rows), then create a batch. Each valid
      row becomes its own transfer with

      `batch_id`; rows that fail validation are skipped and listed with a
      reason.
    x-displayName: Batches
  - name: Scheduled payments
    description: >
      Payments that repeat (or run once on a future date). Each run creates a
      normal transfer, checked like any other.
    x-displayName: Scheduled payments
  - name: Approvals
    description: >
      When a second person on your team must approve. One generic endpoint for
      every customer `ApprovalKind`:

      transfers and batches over the approval threshold, agent payments over an
      agent's ask-first amount, pay runs,

      role changes and limit changes.
    x-displayName: Approvals
  - name: Limits
    description: >-
      Read-only payment limits for the business (per payment, daily, approval
      threshold).
    x-displayName: Limits
  - name: Collections
    description: >-
      Header figures for the Collections screen (outstanding, overdue, collected
      this month, average days to pay) and saved customers (payers).
    x-displayName: Collections
  - name: Invoices
    description: >
      Create, send, chase, void and track invoices. VAT (7.5%) is computed
      server-side. An invoice can have its

      own account number; payments into it are recorded against the invoice
      automatically, including part payments.
    x-displayName: Invoices
  - name: Payment links
    description: >-
      Reusable pay pages at `pay.panoflow.io/<slug>` with a fixed or
      customer-entered amount. Can be paused and turned back on.
    x-displayName: Payment links
  - name: Virtual accounts
    description: >
      Static virtual accounts only: one permanent account number per customer,
      settling into one of your accounts.

      Per-invoice account numbers live on the invoice (`virtual_account`), not
      here.
    x-displayName: Virtual accounts
  - name: Inbound credits
    description: >-
      Money in. Every transfer received into an invoice account, a static
      virtual account, a pay page checkout or one of your accounts, and what it
      was matched to.
    x-displayName: Inbound credits
  - name: People
    description: >-
      Add, list, update and offboard the people you pay, and the deductions
      added to each person.
    x-displayName: People
  - name: Deduction types
    description: >-
      Deductions set up once and applied to everyone or to selected people
      (PAYE, pension, NHF, loans, advances, savings, custom levies).
    x-displayName: Deduction types
  - name: Payroll settings
    description: >-
      Pay day, payslip emails, reminders, automatic pay, the second approver and
      the account runs are paid from.
    x-displayName: Payroll settings
  - name: Pay runs
    description: >
      Prepare a month's run (computes lines), adjust who is paid and one-off
      bonuses, approve with 2FA (and a second

      approver above the threshold), then execute. Execution is asynchronous
      (202); poll the run or wait for

      `payrun.completed`.
    x-displayName: Pay runs
  - name: Payslips
    description: Payslips issued for paid lines. Download as PDF or email to the person.
    x-displayName: Payslips
  - name: OAuth
    description: >-
      OAuth 2.1 authorization server for MCP clients and other apps: metadata,
      dynamic client registration, the consent page's API (2FA at grant time),
      grants (connected apps), token and revocation endpoints. Access tokens
      live 15 minutes; refresh tokens rotate and are bound to the grant.
    x-displayName: OAuth
  - name: Webhooks
    description: >-
      Endpoints that receive signed events (HMAC-SHA256, `PanoFlow-Signature`),
      their signing secrets, test events, and deliveries with response codes and
      latency, retry and replay.
    x-displayName: Webhooks
  - name: Events
    description: >-
      The event log behind webhooks (30 days), replay, and the event-type
      catalogue.
    x-displayName: Events
paths:
  /transfers:
    get:
      tags:
        - Transfers
      summary: List transfers
      description: >-
        Newest first. Includes batch rows, scheduled runs, agent payments and
        reversals made by PanoFlow.


        **Error codes:** `invalid_api_key`, `parameter_invalid`,
        `scope_missing`, `rate_limited`.
      operationId: listTransfers
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/After'
        - $ref: '#/components/parameters/Before'
        - $ref: '#/components/parameters/Query'
        - $ref: '#/components/parameters/CreatedGte'
        - $ref: '#/components/parameters/CreatedLte'
        - $ref: '#/components/parameters/IncludeTotalCount'
        - name: status
          in: query
          description: Comma-separated `MovementStatus` values, e.g. `failed,reversed`.
          schema:
            type: array
            items:
              type: string
            examples:
              - - failed
                - reversed
          style: form
          explode: false
        - name: type
          in: query
          description: Comma-separated `TransferType` values.
          schema:
            type: array
            items:
              type: string
            examples:
              - - payout
          style: form
          explode: false
        - name: account_id
          in: query
          description: Transfers out of, or into, this account.
          schema:
            type: string
        - name: recipient_id
          in: query
          description: Payouts to this recipient.
          schema:
            type: string
        - name: batch_id
          in: query
          description: Rows of this batch.
          schema:
            type: string
        - name: scheduled_payment_id
          in: query
          description: Runs of this scheduled payment.
          schema:
            type: string
        - name: agent_id
          in: query
          description: Transfers created by this agent.
          schema:
            type: string
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: A page of transfers.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferList'
              examples:
                failed:
                  summary: status=failed
                  value:
                    object: list
                    has_more: false
                    next_cursor: null
                    data:
                      - id: trf_01JCQKFBF5KZNWJ47TAN9ZT24M
                        object: transfer
                        type: payout
                        status: failed
                        status_reason: rejected_by_receiving_bank
                        response_code: '08'
                        response_message: Account name mismatch
                        source_account:
                          id: acc_01JCWPQ5E6EYCNDY0YP57RCYBV
                          name: Naira · Main
                          currency: NGN
                          account_number_last4: '6789'
                        recipient_id: rcp_01JCQY77ZXYYK596NGYA1DQ91K
                        counterparty_name: Chidi Eze
                        bank_account:
                          account_number: '3089912245'
                          bank_code: '011'
                          bank_name: First Bank
                          account_name: EZE CHIDI JOHN
                        amount:
                          amount: 15000000
                          currency: NGN
                        fee:
                          amount: 0
                          currency: NGN
                        total:
                          amount: 15000000
                          currency: NGN
                        narration: Lagos dispatch
                        nip_session_id: '000023261025090312847261930475'
                        failure:
                          code: account_name_mismatch
                          message: >-
                            The receiving bank rejected the payment: account
                            number and name don't match. The money is back in
                            Naira · Main.
                        created_by:
                          type: user
                          id: usr_01JCG0X454YG4GFDEXZR4YJ2C4
                          name: Ngozi Uche
                        created_at: '2026-10-25T09:02:00Z'
                        updated_at: '2026-10-25T09:41:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
      security:
        - ApiKey: []
        - OAuth2: []
components:
  parameters:
    Limit:
      name: limit
      in: query
      description: Page size, 1–100.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    After:
      name: after
      in: query
      description: >-
        Cursor from the previous page's `next_cursor`. Returns older items.
        Don't send with `before`.
      schema:
        type: string
    Before:
      name: before
      in: query
      description: >
        Cursor for the reverse direction: returns the items newer than this one.
        The page is still sorted newest first;

        `has_more` and `next_cursor` then continue in the same (newer)
        direction, so pass `next_cursor` as `before` to

        keep paging towards the newest items. Don't send with `after`.
      schema:
        type: string
    Query:
      name: q
      in: query
      description: Free-text search.
      schema:
        type: string
        maxLength: 200
    CreatedGte:
      name: created_gte
      in: query
      description: Only items created at or after this time.
      schema:
        type: string
        format: date-time
    CreatedLte:
      name: created_lte
      in: query
      description: Only items created at or before this time.
      schema:
        type: string
        format: date-time
    IncludeTotalCount:
      name: include
      in: query
      description: Set to `total_count` to add `total_count` to the list response.
      schema:
        type: string
        enum:
          - total_count
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: >
        Optional client correlation id, up to 128 characters. It is not echoed:
        the response's `X-Request-Id` is always

        our own `req_…` id. We log your value with the request as
        `client_request_id`, so support can find a request by

        either id. A longer value is refused with 400 `parameter_invalid`.
      schema:
        type: string
        maxLength: 128
  headers:
    X-Request-Id:
      description: >-
        Our request id (`req_…`), on every response. Always ours, never the
        client's `X-Request-Id`. Quote it to support.
      schema:
        type: string
    X-RateLimit-Limit:
      description: Requests allowed per minute for this key, token or session.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests left in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix time when the window resets.
      schema:
        type: integer
    Retry-After:
      description: Seconds to wait before retrying.
      schema:
        type: integer
  schemas:
    TransferList:
      allOf:
        - $ref: '#/components/schemas/ListEnvelope'
        - type: object
          properties:
            data:
              type: array
              description: Transfers.
              items:
                $ref: '#/components/schemas/Transfer'
    ListEnvelope:
      type: object
      description: >-
        Cursor page. Domain files compose this with `allOf` and set
        `data.items`.
      required:
        - object
        - data
        - has_more
      properties:
        object:
          type: string
          const: list
        data:
          type: array
          items: {}
          description: Items, newest first.
        has_more:
          type: boolean
          description: True when another page exists.
        next_cursor:
          type:
            - string
            - 'null'
          description: >-
            Cursor for the next page in the same direction: pass it as `after`
            if this page was read with `after` (or no cursor), as `before` if it
            was read with `before`. Null when `has_more` is false.
        total_count:
          type: integer
          description: Only when `include=total_count` was sent.
    Transfer:
      type: object
      description: >
        Money leaving (or, for a `reversal`, coming back to) an account.
        Lifecycle is `MovementStatus`.

        A payout always has one source account and, once sent, one NIP session
        id.


        Carries the ISO 8583 outcome (`ResponseOutcome`, CONVENTIONS §7a). Batch
        rows and scheduled-payment runs are

        transfers, so they carry it too:


        | status | `response_code` |

        | --- | --- |

        | `pending_approval`, `scheduled`, `processing` | `09` |

        | `processing` with `status_reason: awaiting_bank_confirmation` (the
        status query can't confirm yet) | `01` |

        | `completed`, `reversed`, and a `reversal` | `00` |

        | `failed` after reaching the bank | the bank's NIP code, e.g. `07`,
        `08`, `51`, `57`, `91`, `97` |

        | `failed` before reaching the bank | ours, from the error mapping in
        errors.yaml (e.g. `51` for `insufficient_balance` on a scheduled run,
        `61` for `limit_exceeded`) |

        | `rejected`, `canceled` (never sent) | `21` |
      allOf:
        - $ref: '#/components/schemas/ResponseOutcome'
      required:
        - id
        - object
        - type
        - status
        - response_code
        - response_message
        - amount
        - fee
        - total
        - created_by
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: '`trf_…`'
        object:
          type: string
          const: transfer
          description: Always `transfer`.
        type:
          $ref: '#/components/schemas/TransferType'
        status:
          $ref: '#/components/schemas/MovementStatus'
        status_reason:
          $ref: '#/components/schemas/TransferStatusReason'
        source_account:
          $ref: '#/components/schemas/AccountSummary'
          description: >-
            Account the money left. Omitted on a `reversal` (the other side is
            not shown).
        destination_account:
          $ref: '#/components/schemas/AccountSummary'
          description: >-
            For `own_account` and `reversal`, this business's account that
            received the money.
        recipient_id:
          type: string
          description: '`rcp_…` when paid to (or saved as) a recipient.'
        counterparty_name:
          type: string
          description: >-
            Who was paid, as the UI shows it ("Kano Freight Services", "Naira ·
            Payroll").
        bank_account:
          $ref: '#/components/schemas/BankAccountRef'
          description: For payouts, the account paid, with the name-enquiry `account_name`.
        amount:
          $ref: '#/components/schemas/Money'
          description: What the recipient receives.
        fee:
          $ref: '#/components/schemas/Money'
          description: >-
            Fee charged on top (zero for own_account). Refunded with the amount
            if the transfer fails.
        total:
          $ref: '#/components/schemas/Money'
          description: Amount + fee taken from the source account.
        narration:
          type: string
          description: Shows on the recipient's bank statement.
        reason:
          type: string
          description: Why it was made (shown to approvers; sent by agents).
        nip_session_id:
          type: string
          pattern: ^\d{30}$
          description: >-
            NIP session id, present once the payout has been sent. Quote it to
            the receiving bank.
        transaction_id:
          type: string
          description: '`txn_…`, the line in the Transactions view and on the receipt.'
        failure:
          $ref: '#/components/schemas/PaymentFailure'
          description: Present when `status` is `failed`.
        approval:
          $ref: '#/components/schemas/Approval'
          description: >-
            The approval, when this transfer needed one (not on batch rows; see
            the batch).
        batch_id:
          type: string
          description: '`bat_…` when this is a row of a batch.'
        batch_line:
          type: integer
          minimum: 1
          description: The row's line number in the batch file.
        scheduled_payment_id:
          type: string
          description: '`sch_…` when this is a run of a scheduled payment.'
        scheduled_for:
          $ref: '#/components/schemas/Date'
          description: Run date, for a `scheduled` transfer.
        agent_id:
          type: string
          description: '`agt_…` when an agent created it.'
        reversal_of:
          type: string
          description: On a `reversal`, the `trf_…` it reverses.
        reversed_by:
          type: string
          description: On a `reversed` transfer, the `trf_…` of the reversal.
        document_ids:
          type: array
          items:
            type: string
          description: '`doc_…` files kept with the payment (invoice, receipt).'
        estimated_arrival_at:
          $ref: '#/components/schemas/Timestamp'
          description: Expected arrival while processing.
        timeline:
          type: array
          description: >-
            Tracking steps, oldest first. Returned on `GET /transfers/{id}`;
            omitted in lists.
          items:
            $ref: '#/components/schemas/TimelineEvent'
        created_by:
          $ref: '#/components/schemas/Actor'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        completed_at:
          $ref: '#/components/schemas/Timestamp'
        metadata:
          $ref: '#/components/schemas/Metadata'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - response_code
            - type
            - code
            - message
            - request_id
          properties:
            response_code:
              $ref: '#/components/schemas/ResponseCode'
            type:
              type: string
              enum:
                - invalid_request
                - authentication
                - permission
                - not_found
                - conflict
                - rate_limited
                - partner
                - internal
              description: >-
                Error class, fixed per `code` (errors.yaml `x-codes` `type`;
                CONVENTIONS §7).
            code:
              $ref: '#/components/schemas/ErrorCode'
            message:
              type: string
              description: >-
                Safe to show the customer. Never reveals screening, fraud or STR
                state.
            param:
              type: string
              description: Dot path of the offending field.
            request_id:
              type: string
              description: '`req_…`'
            doc_url:
              type: string
              format: uri
              description: Page explaining the code.
            details:
              type: object
              additionalProperties: true
              description: Machine-readable extra context for the code.
    ResponseOutcome:
      type: object
      description: >
        The ISO 8583 outcome of a money movement. Every transfer, batch row,
        inbound credit, pay-run line and agent

        payment includes these two fields (compose with `allOf`). By
        `MovementStatus`:

        `draft`, `pending_approval`, `scheduled`, `processing`: `09`.
        `completed`: `00`. `failed`, `rejected`: the

        bank's NIP code (e.g. `51`, `91`), or ours from the error mapping when
        it never reached the bank.

        `canceled`: `21` (no action taken; no money moved). `reversed`: keeps
        the original `00` (the money did settle);

        the reversal is its own linked movement with its own code. `01` means we
        couldn't confirm the outcome with the

        bank yet; the payment is never retried until it resolves.
      required:
        - response_code
        - response_message
      properties:
        response_code:
          $ref: '#/components/schemas/ResponseCode'
        response_message:
          type: string
          description: >-
            Standard meaning of the code (the `x-codes` text), not customer
            copy. Customer-safe wording is in `status_reason` / `error.message`.
          examples:
            - Approved or completed successfully
    TransferType:
      type: string
      enum:
        - payout
        - own_account
        - reversal
      description: >
        - `payout`: to a bank account outside PanoFlow, over NIP.

        - `own_account`: between two of this business's accounts in the same
        currency.

        - `reversal`: created by PanoFlow to reverse a completed transfer;
        read-only for customers and linked by
          `reversal_of`.
    MovementStatus:
      type: string
      description: >
        Lifecycle shared by every money-moving resource (CONVENTIONS §8). UI
        labels: completed → "Settled",

        pending_approval → "Pending approval".
      enum:
        - draft
        - pending_approval
        - scheduled
        - processing
        - completed
        - failed
        - rejected
        - canceled
        - reversed
    TransferStatusReason:
      type: string
      description: >
        Sub-state of `status` (clients must tolerate new values):


        | status | status_reason |

        | --- | --- |

        | `pending_approval` | `awaiting_approval` |

        | `scheduled` | `awaiting_run_date` |

        | `processing` | `queued` (checks and hold done, not yet sent),
        `sent_to_bank`, `awaiting_bank_confirmation` (no answer from the bank; a
        status query is running and it will not be re-sent until it answers) |

        | `completed` | `settled` |

        | `failed` | `rejected_by_receiving_bank`, `name_enquiry_failed`,
        `insufficient_balance`, `limit_exceeded`, `could_not_process` |

        | `rejected` | `rejected_by_approver` |

        | `canceled` | `canceled_by_user`, `approval_expired`,
        `schedule_skipped`, `schedule_canceled`, `batch_canceled` |

        | `reversed` | `reversed` |
      enum:
        - awaiting_approval
        - awaiting_run_date
        - queued
        - sent_to_bank
        - awaiting_bank_confirmation
        - settled
        - rejected_by_receiving_bank
        - name_enquiry_failed
        - insufficient_balance
        - limit_exceeded
        - could_not_process
        - rejected_by_approver
        - canceled_by_user
        - approval_expired
        - schedule_skipped
        - schedule_canceled
        - batch_canceled
        - reversed
    AccountSummary:
      type: object
      description: >-
        Enough of a PanoFlow account to show it ("Naira · Main · ••••6789").
        Full account in the Accounts API.
      required:
        - id
        - name
        - currency
      properties:
        id:
          type: string
          description: '`acc_…`'
        name:
          type: string
          description: >-
            Display label, e.g. "Naira · Main", "Naira · Payroll", "US dollar ·
            Main".
        currency:
          $ref: '#/components/schemas/Currency'
        account_number_last4:
          type: string
          pattern: ^\d{4}$
          description: Last four digits of the account number.
    BankAccountRef:
      type: object
      description: A bank account outside PanoFlow (a recipient or a payer).
      required:
        - account_number
        - bank_code
      properties:
        account_number:
          $ref: '#/components/schemas/NubanAccountNumber'
        bank_code:
          $ref: '#/components/schemas/BankCode'
        bank_name:
          type: string
          description: Display name of the bank.
          examples:
            - GTBank
        account_name:
          type: string
          description: Name on the account as returned by name enquiry.
    Money:
      type: object
      description: >-
        An amount in minor units (kobo, cents) with its currency. Never a
        decimal.
      required:
        - amount
        - currency
      properties:
        amount:
          type: integer
          format: int64
          description: Amount in minor units. ₦450,000.00 is 45000000.
          examples:
            - 45000000
        currency:
          $ref: '#/components/schemas/Currency'
    PaymentFailure:
      type: object
      description: >-
        Customer-safe failure. The money (amount and fee) is back in the source
        account when a transfer fails.
      required:
        - code
        - message
      properties:
        code:
          $ref: '#/components/schemas/PaymentFailureCode'
        message:
          type: string
          description: Safe to show the customer as is.
    Approval:
      type: object
      description: >-
        A request for a second person to approve something. The requester can
        never decide their own approval.
      required:
        - id
        - object
        - kind
        - subject_id
        - status
        - requested_by
        - requested_at
      properties:
        id:
          type: string
          description: '`apr_…`'
        object:
          type: string
          const: approval
        kind:
          $ref: '#/components/schemas/ApprovalKind'
        subject_id:
          type: string
          description: Id of the thing being approved.
        summary:
          type: string
          description: One-line human summary, e.g. "Pay ₦850,000.00 to NGOZI ADEYEMI".
        amount:
          $ref: '#/components/schemas/Money'
        status:
          $ref: '#/components/schemas/ApprovalStatus'
        reason:
          type: string
          description: Why the requester asked.
        decision_note:
          type: string
          description: Note from the person who decided.
        requested_by:
          $ref: '#/components/schemas/Actor'
        requested_at:
          $ref: '#/components/schemas/Timestamp'
        decided_by:
          $ref: '#/components/schemas/Actor'
        decided_at:
          $ref: '#/components/schemas/Timestamp'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    Date:
      type: string
      format: date
      description: Calendar date, interpreted in Africa/Lagos.
      examples:
        - '2026-11-07'
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339, UTC.
      examples:
        - '2026-10-27T08:41:00Z'
    TimelineEvent:
      type: object
      description: One step in a transfer's tracking list.
      required:
        - type
        - label
        - at
      properties:
        type:
          type: string
          enum:
            - created
            - approval_requested
            - approved
            - rejected
            - scheduled
            - sent_to_bank
            - completed
            - failed
            - canceled
            - reversed
          description: What happened.
        label:
          type: string
          description: >-
            Display text, e.g. "Sent to the receiving bank", "Rejected, money
            returned".
        at:
          $ref: '#/components/schemas/Timestamp'
        actor:
          $ref: '#/components/schemas/Actor'
    Actor:
      type: object
      description: Who did something. Shown in activity, approvals and the audit log.
      required:
        - type
        - id
        - name
      properties:
        type:
          type: string
          enum:
            - user
            - staff
            - agent
            - api_key
            - system
          description: Kind of actor.
        id:
          type: string
          description: The actor's id (`usr_…`, `stf_…`, `agt_…`, `key_…`, or `system`).
        name:
          type: string
          description: Display name, e.g. "Adaeze Okafor" or "Procurement agent".
    Metadata:
      type: object
      description: >-
        Up to 20 string keys (≤ 40 chars) with string values (≤ 500 chars).
        Returned on the resource and its events.
      maxProperties: 20
      additionalProperties:
        type: string
        maxLength: 500
    ResponseCode:
      type: string
      pattern: ^[0-9A-Z]{2}$
      description: >
        Two-character ISO 8583 response code, using the NIBSS NIP meanings
        (CONVENTIONS §7a). `00` is success.

        Carried on every error (mapped from `error.code`, see errors.yaml
        `x-codes`) and on every money-movement

        object (the bank's final code, or our own while it is in flight).
        Clients must tolerate codes added later.
      enum:
        - '00'
        - '01'
        - '03'
        - '05'
        - '06'
        - '07'
        - '08'
        - '09'
        - '12'
        - '13'
        - '14'
        - '15'
        - '16'
        - '17'
        - '21'
        - '25'
        - '26'
        - '30'
        - '34'
        - '35'
        - '51'
        - '57'
        - '58'
        - '61'
        - '63'
        - '65'
        - '68'
        - '91'
        - '92'
        - '94'
        - '96'
        - '97'
      x-codes:
        '12': Invalid transaction
        '13': Invalid amount
        '14': Invalid batch number
        '15': Invalid session or record id
        '16': Unknown bank code
        '17': Invalid channel
        '21': No action taken
        '25': Unable to locate record
        '26': Duplicate record
        '30': Format error
        '34': Suspected fraud
        '35': Contact sending bank
        '51': No sufficient funds
        '57': Transaction not permitted to sender
        '58': Transaction not permitted on channel
        '61': Transfer limit exceeded
        '63': Security violation
        '65': Exceeds withdrawal frequency
        '68': Response received too late
        '91': Beneficiary bank not available
        '92': Routing error
        '94': Duplicate transaction
        '96': System malfunction
        '97': Timeout waiting for response from destination
        '00': Approved or completed successfully
        '01': Status unknown, please wait for settlement report
        '03': Invalid sender
        '05': Do not honour
        '06': Dormant account
        '07': Invalid account
        '08': Account name mismatch
        '09': Request processing in progress
    ErrorCode:
      type: string
      description: >-
        Stable machine-readable error code. Clients must tolerate codes added
        later.
      enum:
        - invalid_json
        - parameter_missing
        - parameter_invalid
        - parameter_unknown
        - invalid_api_key
        - session_expired
        - invalid_credentials
        - two_factor_required
        - two_factor_invalid
        - token_expired
        - staff_account_not_allowed
        - role_not_permitted
        - scope_missing
        - step_up_required
        - business_frozen
        - business_not_approved
        - account_closed
        - ip_not_allowed
        - agent_paused
        - test_key_on_live
        - email_not_verified
        - two_factor_enrolment_required
        - csrf_token_invalid
        - resource_not_found
        - idempotency_key_reused
        - invalid_state
        - self_approval_not_allowed
        - already_decided
        - duplicate_resource
        - version_conflict
        - insufficient_balance
        - limit_exceeded
        - daily_limit_exceeded
        - approval_threshold_exceeded
        - recipient_not_allowed
        - recipient_name_mismatch
        - name_enquiry_failed
        - account_number_invalid
        - currency_not_enabled
        - currency_mismatch
        - amount_too_small
        - amount_too_large
        - schedule_in_past
        - batch_row_invalid
        - batch_too_large
        - document_rejected
        - checks_incomplete
        - rate_expired
        - payroll_short_funds
        - webhook_url_invalid
        - password_too_weak
        - password_breached
        - scope_not_allowed
        - application_incomplete
        - account_balance_not_zero
        - verification_code_invalid
        - link_expired
        - invite_expired
        - net_pay_negative
        - notification_channel_locked
        - phone_not_verified
        - rate_limited
        - bank_unavailable
        - identity_provider_unavailable
        - registry_unavailable
        - partner_timeout
        - screening_unavailable
        - messaging_unavailable
        - internal_error
      x-codes:
        invalid_json:
          status: 400
          type: invalid_request
          response_code: '30'
          meaning: Body is not valid JSON.
        parameter_missing:
          status: 400
          type: invalid_request
          response_code: '30'
          meaning: A required field is missing; see `param`.
        parameter_invalid:
          status: 400
          type: invalid_request
          response_code: '30'
          meaning: A field has the wrong type or format; see `param`.
        parameter_unknown:
          status: 400
          type: invalid_request
          response_code: '30'
          meaning: An unknown field was sent.
        invalid_api_key:
          status: 401
          type: authentication
          response_code: '63'
          meaning: >-
            Key unknown, revoked, or its business inactive. Never treated as
            anonymous.
        session_expired:
          status: 401
          type: authentication
          response_code: '63'
          meaning: Session timed out or was revoked.
        invalid_credentials:
          status: 401
          type: authentication
          response_code: '63'
          meaning: Email or password wrong (no hint which).
        two_factor_required:
          status: 401
          type: authentication
          response_code: '63'
          meaning: Password accepted; a 2FA code is needed to finish sign-in.
        two_factor_invalid:
          status: 401
          type: authentication
          response_code: '63'
          meaning: 2FA or backup code wrong or already used.
        token_expired:
          status: 401
          type: authentication
          response_code: '63'
          meaning: OAuth access token expired; refresh it.
        role_not_permitted:
          status: 403
          type: permission
          response_code: '57'
          meaning: The caller's role can't do this.
        scope_missing:
          status: 403
          type: permission
          response_code: '57'
          meaning: The key or token lacks the scope in `details.required_scope`.
        step_up_required:
          status: 403
          type: permission
          response_code: '63'
          meaning: Send a fresh X-Step-Up-Token (2FA within 5 minutes).
        business_frozen:
          status: 403
          type: permission
          response_code: '57'
          meaning: The business is frozen; money can't move. Message stays neutral.
        business_not_approved:
          status: 403
          type: permission
          response_code: '57'
          meaning: Onboarding not approved yet; live money movement is closed.
        account_closed:
          status: 403
          type: permission
          response_code: '57'
          meaning: The account is closed.
        ip_not_allowed:
          status: 403
          type: permission
          response_code: '63'
          meaning: Request came from outside the allowed IP ranges.
        agent_paused:
          status: 403
          type: permission
          response_code: '57'
          meaning: The agent is paused by its owner; its tokens are revoked.
        test_key_on_live:
          status: 403
          type: permission
          response_code: '58'
          meaning: A pk_test_ key was used against a live-only operation.
        resource_not_found:
          status: 404
          type: not_found
          response_code: '25'
          meaning: Not found or not visible to this tenant.
        idempotency_key_reused:
          status: 409
          type: conflict
          response_code: '94'
          meaning: Same Idempotency-Key with a different body.
        invalid_state:
          status: 409
          type: conflict
          response_code: '12'
          meaning: >-
            The resource's status doesn't allow this action; see
            `details.status`.
        self_approval_not_allowed:
          status: 409
          type: conflict
          response_code: '57'
          meaning: The requester can't decide their own approval.
        already_decided:
          status: 409
          type: conflict
          response_code: '21'
          meaning: The approval was already approved, rejected or expired.
        duplicate_resource:
          status: 409
          type: conflict
          response_code: '26'
          meaning: An identical resource already exists (e.g. recipient).
        version_conflict:
          status: 409
          type: conflict
          response_code: '21'
          meaning: The resource changed since it was read; re-read and retry.
        insufficient_balance:
          status: 422
          type: invalid_request
          response_code: '51'
          meaning: Available balance can't cover amount + fee; `details.available`.
        limit_exceeded:
          status: 422
          type: invalid_request
          response_code: '61'
          meaning: Over a per-payment limit; `details.limit`.
        daily_limit_exceeded:
          status: 422
          type: invalid_request
          response_code: '61'
          meaning: Over the daily limit.
        approval_threshold_exceeded:
          status: 422
          type: invalid_request
          response_code: '09'
          meaning: >-
            Over the caller's approval threshold; a pending approval was created
            instead (see `details.approval_id`).
        recipient_not_allowed:
          status: 422
          type: invalid_request
          response_code: '57'
          meaning: Agent tried to pay a recipient outside its allowlist.
        recipient_name_mismatch:
          status: 422
          type: invalid_request
          response_code: '08'
          meaning: >-
            Name on the account doesn't match the name given and confirmation
            wasn't sent.
        name_enquiry_failed:
          status: 422
          type: invalid_request
          response_code: '25'
          meaning: The bank couldn't confirm the account name.
        account_number_invalid:
          status: 422
          type: invalid_request
          response_code: '07'
          meaning: Not a valid NUBAN for that bank.
        currency_not_enabled:
          status: 422
          type: invalid_request
          response_code: '12'
          meaning: Currency not live for this business yet.
        currency_mismatch:
          status: 422
          type: invalid_request
          response_code: '12'
          meaning: Amount currency doesn't match the account's currency.
        amount_too_small:
          status: 422
          type: invalid_request
          response_code: '13'
          meaning: Below the minimum for this payment type.
        amount_too_large:
          status: 422
          type: invalid_request
          response_code: '13'
          meaning: Above the maximum for this payment type.
        schedule_in_past:
          status: 422
          type: invalid_request
          response_code: '12'
          meaning: Scheduled date is not in the future.
        batch_row_invalid:
          status: 422
          type: invalid_request
          response_code: '30'
          meaning: One or more batch rows failed validation; `details.rows`.
        batch_too_large:
          status: 422
          type: invalid_request
          response_code: '14'
          meaning: Batch exceeds the row limit for this plan.
        document_rejected:
          status: 422
          type: invalid_request
          response_code: '30'
          meaning: Upload is the wrong type, too large, or unreadable.
        checks_incomplete:
          status: 422
          type: invalid_request
          response_code: '21'
          meaning: Can't approve; not every KYB check has passed.
        rate_expired:
          status: 422
          type: invalid_request
          response_code: '12'
          meaning: FX quote expired; get a new one.
        payroll_short_funds:
          status: 422
          type: invalid_request
          response_code: '51'
          meaning: Payroll account can't cover the run; `details.short_by`.
        webhook_url_invalid:
          status: 422
          type: invalid_request
          response_code: '30'
          meaning: Webhook URL must be https and publicly reachable.
        password_too_weak:
          status: 422
          type: invalid_request
          response_code: '30'
          meaning: Password fails the strength rules.
        password_breached:
          status: 422
          type: invalid_request
          response_code: '30'
          meaning: Password appears in a known breach.
        email_not_verified:
          status: 403
          type: permission
          response_code: '63'
          meaning: Verify the email address before continuing.
        two_factor_enrolment_required:
          status: 403
          type: permission
          response_code: '63'
          meaning: Set up two-factor authentication before doing anything else.
        staff_account_not_allowed:
          status: 401
          type: authentication
          response_code: '63'
          meaning: >-
            Sign-in with this Google account is not allowed. Not returned to
            customers.
        scope_not_allowed:
          status: 422
          type: invalid_request
          response_code: '57'
          meaning: None of the requested OAuth scopes can be granted to this client.
        application_incomplete:
          status: 422
          type: invalid_request
          response_code: '21'
          meaning: >-
            The onboarding application is missing required details; see
            `details.issues`.
        account_balance_not_zero:
          status: 422
          type: invalid_request
          response_code: '12'
          meaning: Move the balance out before closing the account.
        verification_code_invalid:
          status: 422
          type: invalid_request
          response_code: '63'
          meaning: The verification code is wrong or expired.
        link_expired:
          status: 422
          type: invalid_request
          response_code: '63'
          meaning: The link has expired; request a new one.
        invite_expired:
          status: 422
          type: invalid_request
          response_code: '15'
          meaning: The invite has expired; ask for a new one.
        net_pay_negative:
          status: 422
          type: invalid_request
          response_code: '13'
          meaning: >-
            Deductions exceed gross pay for this person; see
            `details.person_id`.
        notification_channel_locked:
          status: 422
          type: invalid_request
          response_code: '57'
          meaning: >-
            This notification channel can't be turned off (e.g. email for
            `compliance_request`); see `param`.
        phone_not_verified:
          status: 422
          type: invalid_request
          response_code: '63'
          meaning: >-
            Needs a verified phone number first (SMS notifications, SMS recovery
            codes).
        csrf_token_invalid:
          status: 403
          type: permission
          response_code: '63'
          meaning: >-
            A state-changing request made with a session cookie had a missing or
            wrong `X-CSRF-Token`, or came from another origin.
        rate_limited:
          status: 429
          type: rate_limited
          response_code: '65'
          meaning: Too many requests; wait `Retry-After` seconds.
        bank_unavailable:
          status: 503
          type: partner
          response_code: '91'
          meaning: >-
            Partner bank is down or degraded. Retry with the same
            Idempotency-Key.
        identity_provider_unavailable:
          status: 503
          type: partner
          response_code: '96'
          meaning: Identity checks provider unavailable.
        registry_unavailable:
          status: 503
          type: partner
          response_code: '96'
          meaning: >-
            CAC registry unavailable; onboarding continues as "pending
            registry".
        partner_timeout:
          status: 502
          type: partner
          response_code: '97'
          meaning: >-
            A partner (the bank, or the SMS provider) didn't answer in time. For
            payments we confirm the status with the bank before any retry.
        screening_unavailable:
          status: 503
          type: partner
          response_code: '96'
          meaning: >-
            Sanctions and PEP screening is unavailable; nothing was changed.
            Retry with the same Idempotency-Key.
        messaging_unavailable:
          status: 503
          type: partner
          response_code: '96'
          meaning: We couldn't send the email or text message right now. Retry shortly.
        internal_error:
          status: 500
          type: internal
          response_code: '96'
          meaning: Our fault. Safe to retry with the same Idempotency-Key.
    Currency:
      type: string
      description: >-
        ISO 4217. Only NGN is live at launch; others return 422
        `currency_not_enabled`.
      enum:
        - NGN
        - USD
        - GBP
        - EUR
        - GHS
        - KES
    NubanAccountNumber:
      type: string
      description: 10-digit Nigerian NUBAN account number.
      pattern: ^\d{10}$
      examples:
        - '8012457739'
    BankCode:
      type: string
      description: The bank's NIBSS NIP institution code (CBN bank code).
      examples:
        - '058'
    PaymentFailureCode:
      type: string
      description: >
        Why a transfer failed, mapped by the bank adapter from NIP response
        codes and our own checks. Clients must

        tolerate new values.


        - `account_invalid`: the receiving bank says the account doesn't exist
        (e.g. NIP 07).

        - `account_name_mismatch`: the receiving bank rejected the account
        number and name pair.

        - `account_restricted`: the account can't receive payments (dormant,
        blocked, not permitted).

        - `receiving_bank_unavailable`: the receiving bank is unreachable (e.g.
        NIP 91) and the payment wasn't accepted.

        - `receiving_bank_rejected`: any other decline by the receiving bank.

        - `insufficient_balance`: a scheduled run or approved payment couldn't
        be covered when it ran.

        - `limit_exceeded`: a scheduled run would go over a limit when it ran.

        - `could_not_process`: everything else, including problems on PanoFlow's
        side (e.g. NIP 51) and any risk
          or compliance outcome. The message stays neutral and never reveals screening, fraud or STR state.

        The raw NIP code itself is the transfer's `response_code` (e.g. `07` →
        `account_invalid`,

        `08` → `account_name_mismatch`, `57` → `account_restricted`, `91`/`97` →
        `receiving_bank_unavailable`).

        For a risk or compliance outcome `response_code` is `05` (do not
        honour), never `34`, so it doesn't tip off.
      enum:
        - account_invalid
        - account_name_mismatch
        - account_restricted
        - receiving_bank_unavailable
        - receiving_bank_rejected
        - insufficient_balance
        - limit_exceeded
        - could_not_process
    ApprovalKind:
      type: string
      description: >-
        What an approval is for. Customers see `transfer`, `batch`, `pay_run`,
        `agent_payment`, `member_role_change` and `limit_change`; the other
        kinds are PanoFlow's own and never returned to customers.
      enum:
        - transfer
        - batch
        - pay_run
        - agent_payment
        - member_role_change
        - limit_change
        - business_approval
        - business_freeze
        - business_unfreeze
        - payment_reversal
        - ledger_adjustment
        - recon_resolution
        - fee_refund
        - staff_role_change
        - staff_super_admin
        - str_filing
        - pricing_change
        - risk_rule_change
        - feature_flag_change
        - partner_secret_rotation
    ApprovalStatus:
      type: string
      enum:
        - pending
        - approved
        - rejected
        - expired
        - canceled
      description: Approval state.
  responses:
    BadRequest:
      description: Malformed request.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, invalid or expired credentials.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: Authenticated but not allowed (role, scope, step-up, frozen business).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Too many requests.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: >-
        Something went wrong on our side (`internal_error`). Safe to retry with
        the same Idempotency-Key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        X-RateLimit-Limit:
          $ref: '#/components/headers/X-RateLimit-Limit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/X-RateLimit-Remaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/X-RateLimit-Reset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      bearerFormat: pk_live_… / pk_test_…
      description: Server API key, scoped. `pk_test_` keys hit the sandbox tenant.
    OAuth2:
      type: oauth2
      description: >-
        OAuth 2.1 with PKCE. Used by agents (MCP) and third-party apps. Access
        tokens live 15 minutes.
      flows:
        authorizationCode:
          authorizationUrl: https://app.panoflow.io/oauth/authorize
          tokenUrl: https://api.panoflow.io/v1/oauth/token
          refreshUrl: https://api.panoflow.io/v1/oauth/token
          scopes:
            accounts:read: Read balances, accounts and transactions
            transfers:read: Read payments
            transfers:write: Create payments (guardrails-checked for agents)
            recipients:read: Read recipients
            recipients:write: Create recipients (allowlist-checked for agents)
            collections:read: Read invoices, payment links and virtual accounts
            collections:write: Create invoices and payment links

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.