Approve a pay run (2FA)
“Pay N people · ₦…” on the review screen, after the 2FA dialog. Always needs X-Step-Up-Token and an
Idempotency-Key.
First approval (maker), from draft:
- Fails with
422 payroll_short_funds(details.short_by,details.available) if the source account’s available balance cannot cover net pay. Nothing changes; top up and retry. - If net pay is over
second_approval_threshold, the run moves topending_approval(status_reason: awaiting_second_approval) and anapproval(kind: pay_run) is created for the second approver, who is notified. Returns 200. - Otherwise it is approved: with
pay_timing: pay_dayit moves toscheduled(200); withnowit starts executing at once (202, asexecutePayRun).
Second approval (checker), from pending_approval: a different user calling this endpoint decides the
pending approval (same effect as POST /v1/approvals/{approval_id}/approve). The requester can never
approve their own run: 409 self_approval_not_allowed. Funds are checked again. Then scheduled (200) or,
for now, executing (202). Rejection goes through POST /v1/approvals/{approval_id}/reject and moves the
run to rejected.
With auto_pay_when_approved off in settings, a scheduled run waits on pay day for executePayRun.
Error codes: invalid_api_key, parameter_invalid, parameter_missing, resource_not_found, scope_missing, step_up_required, csrf_token_invalid, idempotency_key_reused, self_approval_not_allowed, invalid_state, already_decided, payroll_short_funds, rate_limited.
Authorizations
Server API key, scoped. pk_test_ keys hit the sandbox tenant.
Headers
Required on money-moving POSTs. Same key + same body replays the first response; same key + different body returns 409 idempotency_key_reused. Kept 7 days.
1 - 255Proof of a 2FA step-up within the last 5 minutes (POST /v1/auth/step-up).
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.
128Path Parameters
Pay run id (run_…).
^run_[0-9A-HJKMNP-TV-Z]{26}$Body
Response
Approved; now pending_approval or scheduled.
A month's pay run (run_…).
run_…
Always pay_run.
"pay_run"RFC 3339, UTC.
"2026-10-27T08:41:00Z"
RFC 3339, UTC.
"2026-10-27T08:41:00Z"
Pay month.
^\d{4}-\d{2}$"November 2026 pay run".
Calendar date, interpreted in Africa/Lagos.
"2026-11-07"
CONVENTIONS §8 lifecycle: draft → pending_approval (second approver) → scheduled (waits for pay day)
→ processing → completed | failed; rejected by the second approver; canceled from draft,
pending_approval or scheduled. UI: completed → "Paid". Pay runs are never reversed.
draft, pending_approval, scheduled, processing, completed, failed, rejected, canceled, reversed pay_day: pays on pay_date once approved. now: pays as soon as it is fully approved.
pay_day, now Narration on every salary transfer ("Salary Nov 2026").
One of the business's own PanoFlow accounts (owned by the Accounts service).
Totals over included lines. gross − deductions = net, always.
Net is above second_approval_threshold.
Approve by this time to pay on pay_date ("26 Nov, 4pm").
"2026-10-27T08:41:00Z"
Sub-state.
awaiting_second_approval, awaiting_pay_day, awaiting_execution, changed_after_approval, some_lines_failed, rejected_by_approver Deduction totals by type, with where each goes. Stays in the source account; PanoFlow does not remit them.
Whether the source account covers the run, worked out at read time. Absent once executing.
An amount in minor units (kobo, cents) with its currency. Never a decimal.
The pending or decided approval (kind is pay_run), when one was needed.
Everyone who approved, in order ("Approved by Adaeze Okafor and Tolu Martins").
Who did something. Shown in activity, approvals and the audit log.
RFC 3339, UTC.
"2026-10-27T08:41:00Z"
RFC 3339, UTC.
"2026-10-27T08:41:00Z"
One per person. Returned by getPayRun and run actions; omitted from lists.