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

# Skip the next run

> Cancels the pending `scheduled` transfer (`status_reason: schedule_skipped`) and schedules the run after it.
Skipping the only run of a `once` schedule completes the schedule as `canceled`. Only from `active`
(`409 invalid_state`); a run that has already started can't be skipped.

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



## OpenAPI

````yaml /api-reference/openapi.yaml post /scheduled-payments/{scheduled_payment_id}/skip-next
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:
  /scheduled-payments/{scheduled_payment_id}/skip-next:
    parameters:
      - $ref: '#/components/parameters/ScheduledPaymentId'
    post:
      tags:
        - Scheduled payments
      summary: Skip the next run
      description: >-
        Cancels the pending `scheduled` transfer (`status_reason:
        schedule_skipped`) and schedules the run after it.

        Skipping the only run of a `once` schedule completes the schedule as
        `canceled`. Only from `active`

        (`409 invalid_state`); a run that has already started can't be skipped.


        **Error codes:** `invalid_api_key`, `parameter_invalid`,
        `resource_not_found`, `scope_missing`, `csrf_token_invalid`,
        `idempotency_key_reused`, `invalid_state`, `rate_limited`.
      operationId: skipNextScheduledPayment
      parameters:
        - $ref: '#/components/parameters/IdempotencyKeyOptional'
        - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Skipped; `next_run_on` is the run after.
          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/ScheduledPayment'
              examples:
                rent:
                  $ref: '#/components/examples/ScheduledRent'
        '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'
      security:
        - ApiKey: []
        - OAuth2: []
components:
  parameters:
    ScheduledPaymentId:
      name: scheduled_payment_id
      in: path
      required: true
      description: '`sch_…`'
      schema:
        type: string
        pattern: ^sch_[0-9A-HJKMNP-TV-Z]{26}$
      example: sch_01JC8XE6SZAEAVSNTCPM5Q1NXW
    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:
    ScheduledPayment:
      type: object
      description: A payment to a saved recipient on a schedule.
      required:
        - id
        - object
        - name
        - status
        - recipient_id
        - source_account
        - amount
        - schedule
        - schedule_description
        - run_count
        - created_by
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: '`sch_…`'
        object:
          type: string
          const: scheduled_payment
          description: Always `scheduled_payment`.
        name:
          type: string
          maxLength: 80
          description: e.g. "Warehouse rent".
        status:
          $ref: '#/components/schemas/ScheduledPaymentStatus'
        recipient_id:
          type: string
          description: '`rcp_…`'
        counterparty_name:
          type: string
          description: The recipient's display name.
        bank_account:
          $ref: '#/components/schemas/BankAccountRef'
          description: The recipient's account.
        source_account:
          $ref: '#/components/schemas/AccountSummary'
        amount:
          $ref: '#/components/schemas/Money'
        narration:
          type: string
          description: Narration on every run.
        schedule:
          $ref: '#/components/schemas/Schedule'
        schedule_description:
          type: string
          description: >-
            Human text, e.g. "Every 3 months", "Every Monday", "Monthly, on the
            28th".
        next_run_on:
          $ref: '#/components/schemas/Date'
          description: Next run date. Omitted when paused, canceled or completed.
        next_transfer_id:
          type: string
          description: The `scheduled` transfer for the next run.
        last_run:
          $ref: '#/components/schemas/LastRun'
        run_count:
          type: integer
          minimum: 0
          description: Runs so far.
        created_by:
          $ref: '#/components/schemas/Actor'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
        metadata:
          $ref: '#/components/schemas/Metadata'
    ScheduledPaymentStatus:
      type: string
      enum:
        - active
        - paused
        - canceled
        - completed
      description: >
        State of the schedule itself (its runs are transfers with
        `MovementStatus`). `completed` when a `once`

        schedule has run or `end_on` has passed.
    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.
    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.
    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'
    Schedule:
      type: object
      description: >
        When it runs. Examples: "Every Monday" = weekly, `day_of_week: monday`;
        "Monthly, on the 28th" = monthly,

        `day_of_month: 28`; "Every 3 months" = monthly, `interval: 3`. Runs at
        08:00 Africa/Lagos. A

        `day_of_month` past the end of a month runs on the month's last day.
      required:
        - frequency
        - start_on
      properties:
        frequency:
          type: string
          enum:
            - once
            - weekly
            - monthly
          description: '`once` runs on `start_on` only.'
        interval:
          type: integer
          minimum: 1
          maximum: 12
          default: 1
          description: Every N weeks or months.
        day_of_week:
          type: string
          enum:
            - monday
            - tuesday
            - wednesday
            - thursday
            - friday
            - saturday
            - sunday
          description: Weekly only.
        day_of_month:
          type: integer
          minimum: 1
          maximum: 31
          description: Monthly only.
        start_on:
          $ref: '#/components/schemas/Date'
          description: First run on or after this date. Must be in the future.
        end_on:
          $ref: '#/components/schemas/Date'
          description: No runs after this date. Omit to repeat until canceled.
    Date:
      type: string
      format: date
      description: Calendar date, interpreted in Africa/Lagos.
      examples:
        - '2026-11-07'
    LastRun:
      type: object
      description: >-
        The most recent run, with its transfer's ISO 8583 outcome
        (`ResponseOutcome`).
      allOf:
        - $ref: '#/components/schemas/ResponseOutcome'
      required:
        - 'on'
        - transfer_id
        - status
        - response_code
        - response_message
      properties:
        'on':
          $ref: '#/components/schemas/Date'
        transfer_id:
          type: string
          description: '`trf_…`'
        status:
          $ref: '#/components/schemas/MovementStatus'
    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".
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339, UTC.
      examples:
        - '2026-10-27T08:41:00Z'
    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.
    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
    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
    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
    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.
  examples:
    ScheduledRent:
      summary: Warehouse rent, every 3 months
      value:
        id: sch_01JC8XE6SZAEAVSNTCPM5Q1NXW
        object: scheduled_payment
        name: Warehouse rent
        status: active
        recipient_id: rcp_01JCQY77ZXYYK596NGYA1DQ91K
        counterparty_name: Garki Properties
        bank_account:
          account_number: '0021345516'
          bank_code: '221'
          bank_name: Stanbic IBTC
          account_name: GARKI PROPERTIES LTD
        source_account:
          id: acc_01JCWPQ5E6EYCNDY0YP57RCYBV
          name: Naira · Main
          currency: NGN
          account_number_last4: '6789'
        amount:
          amount: 275000000
          currency: NGN
        narration: Warehouse rent
        schedule:
          frequency: monthly
          interval: 3
          day_of_month: 1
          start_on: '2027-01-01'
        schedule_description: Every 3 months
        next_run_on: '2027-01-01'
        next_transfer_id: trf_01JCNPZX45HY43KWJRP1XPA7Z3
        run_count: 0
        created_by:
          type: user
          id: usr_01JCMZ9J92V81E5128Q6RW31FZ
          name: Adaeze Okafor
        created_at: '2026-10-28T08:40:00Z'
        updated_at: '2026-10-28T08:40:00Z'
  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'
  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.