> ## 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.

# Close a static virtual account

> `active` → `closed`. The number is closed at the bank, so new transfers to it are rejected by the bank
and never reach PanoFlow. History stays. Can be reopened. Partner call (503 on failure; nothing changes).

**Error codes:** `invalid_api_key`, `parameter_invalid`, `resource_not_found`, `scope_missing`, `csrf_token_invalid`, `idempotency_key_reused`, `bank_unavailable`, `rate_limited`.



## OpenAPI

````yaml /api-reference/openapi.yaml post /collections/virtual-accounts/{virtual_account_id}/close
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:
  /collections/virtual-accounts/{virtual_account_id}/close:
    parameters:
      - $ref: '#/components/parameters/VirtualAccountId'
    post:
      tags:
        - Virtual accounts
      summary: Close a static virtual account
      description: >-
        `active` → `closed`. The number is closed at the bank, so new transfers
        to it are rejected by the bank

        and never reach PanoFlow. History stays. Can be reopened. Partner call
        (503 on failure; nothing changes).


        **Error codes:** `invalid_api_key`, `parameter_invalid`,
        `resource_not_found`, `scope_missing`, `csrf_token_invalid`,
        `idempotency_key_reused`, `bank_unavailable`, `rate_limited`.
      operationId: closeVirtualAccount
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: The closed account.
          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/VirtualAccount'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/PartnerUnavailable'
      security:
        - ApiKey: []
        - OAuth2: []
components:
  parameters:
    VirtualAccountId:
      name: virtual_account_id
      in: path
      required: true
      description: Static virtual account id (`sva_…`).
      schema:
        type: string
        pattern: ^sva_[0-9A-HJKMNP-TV-Z]{26}$
      example: sva_01JYK596NGYA1DQ91K5GQAPENE
    IdempotencyKeyOptional:
      name: Idempotency-Key
      in: header
      required: false
      description: Optional on non-money POSTs; honoured when sent.
      schema:
        type: string
        minLength: 1
        maxLength: 255
    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:
    VirtualAccount:
      type: object
      description: >-
        A static virtual account (`sva_…`), a permanent account number for one
        customer.
      required:
        - id
        - object
        - created_at
        - updated_at
        - type
        - customer
        - account_name
        - account_number
        - bank_name
        - currency
        - settlement_account
        - status
        - payment_count
        - amount_collected
      properties:
        id:
          type: string
          description: '`sva_…`'
        object:
          type: string
          const: virtual_account
          description: Always `virtual_account`.
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        type:
          type: string
          const: static
          description: Always `static`. Per-invoice accounts are on the invoice.
        customer:
          $ref: '#/components/schemas/CustomerRef'
        reference:
          type: string
          description: Your own id for the customer.
        account_name:
          type: string
          description: Name the sender's bank shows.
        account_number:
          $ref: '#/components/schemas/NubanAccountNumber'
        bank_name:
          type: string
          description: Partner bank.
        bank_code:
          $ref: '#/components/schemas/BankCode'
        currency:
          $ref: '#/components/schemas/Currency'
        settlement_account:
          $ref: '#/components/schemas/AccountRef'
        status:
          $ref: '#/components/schemas/VirtualAccountStatus'
        payment_count:
          type: integer
          description: Credits received.
        amount_collected:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Total received.
        last_payment_at:
          $ref: '#/components/schemas/Timestamp'
        closed_at:
          $ref: '#/components/schemas/Timestamp'
        metadata:
          $ref: '#/components/schemas/Metadata'
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339, UTC.
      examples:
        - '2026-10-27T08:41:00Z'
    CustomerRef:
      type: object
      description: The customer (payer) on an invoice or static virtual account.
      required:
        - name
      properties:
        id:
          type: string
          description: Saved customer id (`cus_…`).
        name:
          type: string
          maxLength: 120
          description: Business or person name.
        email:
          $ref: '#/components/schemas/Email'
    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'
    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
    AccountRef:
      type: object
      description: >-
        One of the business's own PanoFlow accounts (owned by the Accounts
        service).
      required:
        - id
        - name
      properties:
        id:
          type: string
          description: Account id (`acc_…`).
        name:
          type: string
          description: Display name, e.g. "Naira · Main" or "Naira · Payroll".
        account_number:
          $ref: '#/components/schemas/NubanAccountNumber'
    VirtualAccountStatus:
      type: string
      description: >-
        `active` accepts transfers; `closed` does not (the bank rejects them).
        Closed accounts can be reopened.
      enum:
        - active
        - closed
    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'
    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
    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.
    Email:
      type: string
      format: email
      maxLength: 254
    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.
  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'
    NotFound:
      description: Not found, or not visible to this tenant.
      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'
    Conflict:
      description: State conflict (idempotency key reused, invalid state, self-approval).
      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'
    PartnerUnavailable:
      description: >-
        A partner (bank, identity, registry, screening, messaging) is
        unavailable or didn't answer in time. 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'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      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.