# Guides - [Introduction](/docs): One API for collections and payouts across Rwanda, Uganda and the EU. - **Get started** - [Quickstart](/docs/quickstart): A test key to a settled charge, in about ten minutes. - [Authentication](/docs/authentication): API keys, scopes, and the things a key deliberately cannot do. - [Sandbox](/docs/sandbox): Test keys, simulated rails, and amounts that fail on purpose. - [Fees](/docs/fees): How InversePay's fee is set, where it shows up, and how to quote it before you charge. - **Take money in** - [Send a payment link](/docs/payment-links): Collect a payment without writing an integration at all. - **Send money out** - [Pay somebody](/docs/payouts): Send money to a mobile money wallet, a bank account or a biller. - [Save a recipient](/docs/beneficiaries): Name-checked, screened, and reusable — the way to pay a bank account twice. - [Pay many people](/docs/bulk-payouts): Batches, recurring schedules and payroll runs — validated before a shilling moves. - [Pay a bill](/docs/bill-payments): Electricity, airtime, pay TV and tax — with the account checked before the money goes. - **Know what happened** - [Verify a webhook](/docs/webhooks): Prove a delivery came from us, including while a secret is rotating. - **Reference** - API reference - Approvals - [List approval thresholds](/docs/api/approvals/list_policies) - [Add an approval threshold](/docs/api/approvals/create_policy): Add a threshold. It applies to movements raised after this call. Anything already waiting keeps the rule it was raised under - an approver who has already agreed should not find their approval silently no longer counted. - [Retire an approval threshold](/docs/api/approvals/retire_policy): Take a threshold out of use. Flagged inactive rather than deleted, so "what control was in force in March" stays answerable. - [Movements awaiting a signature](/docs/api/approvals/list_approvals): Everything waiting on a decision, newest first. Defaults to `pending` because that is the working queue; the decided ones are an archive somebody goes looking for, and defaulting to all of them would bury today's three payouts under last quarter's four hundred. - [One approval, with its decisions](/docs/api/approvals/get_approval): The request and every answer recorded against it so far. A pending request with no decisions is normal — it means nobody has looked yet, which is different from somebody having declined. - [Sign off on a movement](/docs/api/approvals/approve_request): Record one approval; the request completes when enough have agreed. Approving does not move the money. `assert_executable` is a separate call made immediately before the movement runs, because between the last signature and that moment the funds can have left by another route. 409 when the request is already decided, and 403 when the caller raised it: the person who initiated a movement may not approve their own. - [Refuse a movement](/docs/api/approvals/reject_request): One rejection ends the request. Not a vote: a movement somebody with authority has objected to should not proceed because two others outnumbered them. The money reserved against it is released by whatever raised the request, not here. - Activity - [Who did what, and when](/docs/api/activity/list_activity): The audit trail for this business, newest first. Ordered by `occurred_at` and then by id. Entries written in one transaction share a timestamp — it is the transaction's — so ordering by time alone would page inconsistently and could show or skip a row. Refused actions appear alongside successful ones. Filtering them out would remove the entries an investigation is usually looking for. - Charges: Taking money in. One charge is one collection; the rail is chosen from the corridor, so you never name a partner. - [List charges](/docs/api/charges/list_charges): Most recent first, scoped to your business and your key's mode. - [Create a charge](/docs/api/charges/create_charge): Take a payment. A repeated `idempotency_key` returns the original charge with 201 rather than a conflict. A client that did not hear our answer and asked again wants the charge, not an error it has to interpret. - [Quote the fee on a charge](/docs/api/charges/quote_charge_fee): InversePay's fee on a charge, from your plan, before you open it. Priced exactly as the charge itself will be, so the quote is the fee. To put your own margin on a sale, decide what you want to keep and ask with `net_minor`. On a plan charging 2%, say, keeping 10,000 RWF means charging 10,205 RWF. The fee is on the amount the payer pays, so adding 2% to your price falls just short. `price` is the plan the quote was priced on, so you can price any other amount without asking again: the percentage of the amount, rounded down, plus the flat part, then no less than the floor and no more than the cap. - [Fetch a charge](/docs/api/charges/get_charge): Scoped to your business and your key's mode. Someone else's charge and a charge that does not exist give the same 404, on purpose — telling them apart tells an attacker which ids are real. - Business Context - [Businesses the caller belongs to](/docs/api/business-context/list_memberships): Every business this user holds a role in. Requires only a consumer token, because this is how somebody discovers what they can switch into. - [Issue a session token scoped to a business](/docs/api/business-context/switch_business_context): Mint a new token scoped to `business_id`. A new token rather than an amended one: a JWT is signed, so it cannot be re-scoped in place, and issuing a fresh one is what stops the previous scope applying to subsequent calls. Membership is checked here as well as on every later request. Refusing at the point of switching gives the user a clear answer instead of a token that fails on first use. - [What the current token is scoped to](/docs/api/business-context/current_business_context): The business this session acts for, with the role the server read now. Useful to a dashboard on load, and the smallest endpoint that demonstrates scoping: it names a business without the caller having named one. - [This business's profile](/docs/api/business-context/get_business_profile): Read-only for anybody who can see the business. Reading the profile is not managing it: a Viewer needs to know which business they are looking at and how to contact it, and gating that behind the settings permission would leave them staring at a blank page. - [Update the editable part of the profile](/docs/api/business-context/update_business_profile): Change what is not locked. Only fields actually present in the request are written, so a client sending back a partially rendered form cannot blank what it never showed. `legal_name` and `registration_number` are not accepted at all — they are locked after verification and changing one is E3-6's re-verification request, not a profile edit. Audit-logged with before and after, because a change of contact email is how an account is taken over quietly. - Wallets - [Balances this business holds, per currency](/docs/api/wallets/read_balances): Per currency, from the ledger. Readable by anybody who can see the business. A Viewer exists to look at the numbers, and a finance permission on the balance would leave them with a dashboard that shows everything except the thing it is for. - K Y B Documents - [The KYB document checklist with status and version history](/docs/api/kyb-documents/list_kyb_documents): Every document slot for the business, with its current status and the retained prior versions - the status history, newest version first. - [Submit a KYB document (or resubmit after rejection)](/docs/api/kyb-documents/submit_kyb_document): Validate, virus-scan, store and record one document upload. A resubmission is the same call with the same `document_type`: it mints the next version and keeps every earlier one, so a rejected copy remains retrievable. The file must be PDF, JPEG or PNG and within `KYB_DOC_MAX_BYTES`; the content type is checked from the bytes, never from the client. `expires_at` is optional and belongs to this version alone — E3-9. A renewal is a fresh upload with a later date, so the new file's expiry never inherits from the one it replaces. A date already past is refused: that files a document expired on arrival. - Members - [List the team](/docs/api/members/list_members) - [List invitations](/docs/api/members/list_invitations): Outstanding invitations, and recently finished ones, newest first. Separate from `GET /members` because an invitation is no longer a member row. A team screen that wants both asks for both. - [Invite somebody into the business](/docs/api/members/invite_member): Invite the holder of an email address — IN-341. This used to resolve the address to an existing account and refuse if there was none, which meant the common case at a growing business, a colleague who has never heard of us, could not be invited at all. An invitation is now its own record keyed to the address, so no account has to exist yet. Re-inviting an address that already has an invitation outstanding revokes the old one and sends a fresh link. That is also the only way to resend, because the token is hashed and the original can never be shown again. - [Cancel an outstanding invitation](/docs/api/members/revoke_invitation): Kill the link. An Owner who invited the wrong address had no way to undo it before this, and waiting seven days is not an answer when the typo landed at a competitor's domain. - [Accept an invitation addressed to you](/docs/api/members/accept_invitation): Accepting is the invited person's own act — IN-341. **This takes a plain signed-in user, not a business context, and that is the whole fix.** It used to depend on `require_permission`, which resolves through `get_business_context`, which refuses a member who is not yet ACTIVE. The only way to become scoped to a business is `/context/switch`, which refuses that same member with "Accept your invitation first". The endpoint could only ever be reached by somebody who had already accepted. The token identifies both the invitation and the business, so nothing is taken from the caller but their identity. - [Change a member's role](/docs/api/members/change_member_role) - [Suspend or reinstate a member](/docs/api/members/change_member_status) - [Remove a member](/docs/api/members/remove_member): Remove somebody's role. Their business access ends on their next request - the membership is read per request, so there is no cached grant to wait out. - Business Registration - [Register a business and begin onboarding](/docs/api/business-registration/register_business): Create the business at tier B0 with the caller as its active Owner. The registering user is promoted to Owner, exactly as in E2-1: creating a business never mints a second identity - it reuses the `users` row the caller already has. A duplicate registration number in the same country is refused with a 409 and the next steps spelled out: whoever holds that number in that registry is one company, and the second filing is either a mistake or someone else's business. - Sessions - [List your active sessions](/docs/api/sessions/list_my_sessions) - [Sign out of this business everywhere](/docs/api/sessions/revoke_all_my_sessions): End every session this member holds for this business. Takes effect on the next request rather than after some delay: every token issued before this moment is refused, including the refresh tokens that would otherwise keep minting access tokens for days. - [Sign out one device](/docs/api/sessions/revoke_one_session): End a single sign-in, leaving the others alone — IN-335. The case `revoke-all` cannot serve: a member who left a laptop somewhere wants that one cut off, not to be signed out of the machine they are asking from. Two things have to happen and only one of them is obvious. Deleting the session record takes it off this list; **it does not sign anybody out**, because nothing on the request path consults these records. What stops the device is `TokenService.revoke_session`, which denies the token's `sid` — and reaches the refresh token too, since it carries the same claim. Without that second step this endpoint would report success and leave the device working, which is the failure `revoke_all_user_sessions` was written to stop repeating. 404 for a session that is not this member's. The Redis key is `session:{user_id}:{session_id}`, so a caller can only ever name their own; the lookup is what turns someone else's id into "not found" rather than a silent no-op that looks like success. - Mfa - [Your authenticator status](/docs/api/mfa/mfa_status): Whether this member has an authenticator, and how much rope is left. `enrolled` without `active` means a setup was started and never confirmed — the screen should offer to finish it rather than to start again, because starting again issues a new secret and invalidates the QR code already on screen. `recovery_codes_remaining` is here because running out and *then* losing the phone is the one state nobody can recover from unaided, and a member cannot be warned about a number they are never shown. - [Remove your authenticator](/docs/api/mfa/remove): Turn the factor off, on proof. A code is required because a hijacked session that can silently remove the second factor has already removed it. A recovery code is accepted here too — someone whose phone is gone still needs a way out, and that is what recovery codes are for. - [Start setting up an authenticator](/docs/api/mfa/enrol): Generate a secret. This does **not** switch the factor on. Nothing is protected until `/mfa/activate` has seen a code derived from this secret. That order is deliberate: a QR code can be mis-scanned or scanned into nothing, and switching on a factor the member's app cannot actually produce locks them out of their own account. - [Confirm the code and switch it on](/docs/api/mfa/activate): Prove the app works, then turn the factor on. Returns recovery codes. They are shown once and stored hashed, so a member who does not keep them and then loses their phone needs support to intervene — which is why the response is the only place they appear and why the screen showing them has to say so. - [Payout amounts that require a code](/docs/api/mfa/list_step_up): What each currency's step-up threshold is, and whether you set it. Every supported currency is listed, including the ones falling back to the platform default, because a settings screen has to show what *is* happening rather than only what has been overridden. - [Go back to the default threshold](/docs/api/mfa/clear_step_up): Drop your threshold so the platform default applies again. Deliberately not the same as setting it to zero: zero asks for a code on every payout, and without a way back a business that tried that could never undo it. - [Change when a code is required](/docs/api/mfa/set_step_up): Set this business's threshold for one currency. Gated on `BUSINESS_SETTINGS_MANAGE` rather than `MFA_MANAGE_OWN`, and the difference matters: enrolling *your own* authenticator is nobody's business but yours, while deciding when the whole business is asked for one is an administrative control. Reading it stays open to everyone, so a Finance user can see why they were asked. - K Y B Screening - [The business's stakeholders with their screening status](/docs/api/kyb-screening/list_related_persons) - [Declare a stakeholder (UBO/Director) for screening](/docs/api/kyb-screening/add_related_person): Record a related person. Nothing is screened here - the explicit verify call is what creates the Sumsub applicant and runs the AML engine, so declaring somebody the flow never verifies costs nothing. - [Verify identity and screen for sanctions/PEP](/docs/api/kyb-screening/verify_related_person): Screen one stakeholder and re-evaluate the application. Sumsub verifies the identity document - the person is an applicant linked to the business's company applicant as a beneficiary - and then the AML engine screens for sanctions/PEP. The two acceptance criteria are decided here: a hit parks the business `in_review` with a case record; an all-clear application with approved documents auto-progresses to `active` at tier B1. - [The verification record: status, tier and per-person readiness](/docs/api/kyb-screening/current_kyb_verification) - [Send a rejected application back for review](/docs/api/kyb-screening/resubmit_kyb): Put a rejected application back in the compliance review queue — IN-367. Refiling a document resets that document only; the application stays rejected, and out of the reviewer's queue, until this is called. It moves the verification to `in_review` and the business to `pending`, the state a reviewer's request for more information leaves, and records when it came back. The tier stays where rejection left it until a reviewer approves. Answers with the verification record, as `GET /kyb/verification` does. - K Y B Industry - [The current industry and its policy consequence](/docs/api/kyb-industry/current_industry) - [Select the business's industry and apply the industry policy](/docs/api/kyb-industry/select_industry): Record the industry and re-evaluate the application. A restricted industry flags the application `requires_underwriting` and opens an underwriting case record in the same transaction; an allowed industry leaves the application where it stands. The evaluation is idempotent: re-selecting the same industry does not stack cases. - K Y B Re verification - [The business's locked-field change requests](/docs/api/kyb-re-verification/list_field_change_requests) - [Submit a locked-field change (forces re-verification)](/docs/api/kyb-re-verification/submit_field_change_request): File a change to `legal_name` or `registration_number`. When the business is verified the fields are locked: the change is recorded as a pending request, the application moves `in_review`, and live limits drop to the configured safe mode (AC1). The new value is not written to the business - applying it is a compliance operator's decision. When the business is not yet verified the field is not locked and the edit is applied immediately. - A P I Keys: Issuing, scoping, rotating and revoking credentials. - [List the business's API keys](/docs/api/api-keys/list_api_keys): Every key for this business, revoked ones included. Revoked keys stay visible: after an incident the question is which keys existed and when each stopped working, and hiding them answers neither. Secrets are absent from this response shape entirely. - [Create an API key (secret shown once)](/docs/api/api-keys/create_api_key): Issue a key for the business in the token. The response is the only time the secret exists outside the caller's system. Afterwards only its SHA-256 digest and prefix are stored. - [Regenerate an API key, with an overlap window](/docs/api/api-keys/regenerate_api_key): Issue a replacement and let the old key expire, rather than killing it. The old key keeps authenticating for the configured overlap window (`API_KEY_REGENERATE_OVERLAP_MINUTES`), so the new one can be rolled out without a synchronised deploy. As with creation, the plaintext appears in this response and nowhere else, ever. - [Revoke an API key](/docs/api/api-keys/revoke_api_key): Revoke a key. The row remains as evidence that it existed. - Payment links: A URL that collects a payment, with no integration at all. - [List payment links](/docs/api/payment-links/list_links) - [Create a payment link](/docs/api/payment-links/create_link) - [Get a payment link](/docs/api/payment-links/get_link) - [Turn a link off](/docs/api/payment-links/disable_link) - [Turn a link on](/docs/api/payment-links/enable_link): Undo a disable. Does not revive an expired or already-paid link: those describe what happened rather than a switch someone flipped. - Refunds: Returning money from a charge that succeeded. - [List refunds for a charge](/docs/api/refunds/list_refunds) - [Refund a charge](/docs/api/refunds/create_refund): Give money back — AC 1 and AC 2. 409 rather than 400 for an over-refund: the request is well formed and would have been accepted against a different state of the charge. - [Get a refund](/docs/api/refunds/get_refund) - Disputes - [Disputes against your charges](/docs/api/disputes/list_disputes): Every dispute raised against this business, newest first. Each row carries the disputed charge's amount and reference, so a list does not need a call per row to say what is at stake. `open_count` is computed over all of this business's disputes rather than the page: the page is filtered and paged, and a count taken from it would say "3 open" when it meant "3 open on this screen". - [One dispute](/docs/api/disputes/get_dispute): A dispute belonging to another business answers 404, not 403 — a different answer for the two confirms it exists. - [Submit evidence for a dispute](/docs/api/disputes/submit_evidence): Add to what has already been submitted. Merged rather than replaced: a business that uploads a receipt and later a delivery note has provided two things, and a PUT-shaped endpoint would discard the first without saying so. 409 once the dispute is resolved or closed. Submitting into a decided case would record evidence nobody will read and imply it might still change the outcome. - [Accept liability for a dispute](/docs/api/disputes/accept_dispute): Concede, and stop spending time on it. **This does not move money.** Accepting liability resolves the dispute in the payer's favour; the funds move when whatever settles it does so. Refunding here would return money that the card network may also claw back — the same amount twice, and a reconciliation nobody can explain. 409 once the dispute is already resolved or closed. - Invoices: A request for payment with a due date, reminders and a status. - [List invoices](/docs/api/invoices/list_invoices) - [Create a draft invoice](/docs/api/invoices/create_invoice) - [Get an invoice](/docs/api/invoices/get_invoice) - [Edit a draft invoice](/docs/api/invoices/update_invoice) - [Send an invoice](/docs/api/invoices/send_invoice): Issue it and make it payable — AC 1. Returns the payment link slug, which is what the customer opens. - [Void an invoice](/docs/api/invoices/void_invoice): Withdraw it. Its link stops accepting payment and reminders stop. - [What has been paid against this invoice](/docs/api/invoices/list_invoice_payments): Both sources in one list, newest first. "What paid this?" is one question, so it gets one answer. A business chasing a discrepancy should not have to know that an online payment is a charge and a bank transfer is not — though `source` says which, for anyone who does. Only settled, non-test payments appear, so the rows always add up to `paid_minor`. A pending charge has paid nothing yet, and listing it would create exactly the discrepancy this is meant to explain. - [Record a payment made outside InversePay](/docs/api/invoices/record_external_payment): Settle an invoice paid by bank transfer, cash or cheque. **No ledger entry and no balance change.** The money went to the business's own account; we never held it, and crediting a wallet for it would claim funds that are not there. This settles the *invoice* and says nothing about the balance — which is the truth, and the reason this is not recorded as a charge. Partial payments are ordinary: send each one as it arrives and the invoice moves to `paid` when they add up. Overpayment is capped at the total by the reconcile rather than refused, because the money did arrive. - [Set the reminder schedule](/docs/api/invoices/set_reminders): Replace the schedule — AC 2. Reminders stop on payment or void without being cancelled here: that is a property of the invoice's state, not a list someone has to remember to clear. - Insights - [What is worth knowing right now](/docs/api/insights/list_insights): Observations about this business's figures, most serious first. Each one is a named rule over data this API already returns — an overcommitted currency, failed payouts, movements waiting on a signature, open disputes, overdue invoices, a material change in flow, a material FX move on something actually held. An empty list means nothing fired, which is the common and correct answer for a business with nothing wrong. It should render as reassurance, not as a loading state. - Transactions - [Everything that moved](/docs/api/transactions/list_transactions): One feed of every movement, newest first, paged exactly. Ordered and paged on `sequence`, a globally unique identity column, so no row is shown twice or skipped no matter what is written while somebody reads. The previous approach — merging `/charges` with `/payouts` on the client — could not offer that, because the two share no ordering. Readable by anybody who can see the business. A Viewer exists to look at the numbers, and a feed of what the business has done is the plainest form of that. 404 `NO_WALLET` when the business has no wallet: it has no book yet, which is a state to explain rather than an empty list that looks like no activity. - Virtual accounts: A dedicated account number that credits your wallet. - [List virtual accounts](/docs/api/virtual-accounts/list_accounts) - [Open a virtual account](/docs/api/virtual-accounts/create_account) - [Currencies you can open an account in](/docs/api/virtual-accounts/openable_options): The currencies `POST /virtual-accounts` can open on its own. Most schemes need an account number only the partner can mint, so creating one is really *recording* correspondence you already hold. The currencies listed here are the ones where that is not true — we issue the reference ourselves and there is a live rail behind it — so they are the only ones a business can open unaided. Computed per request from the live rail table, so a corridor going live or a rail being switched off changes the answer without a deploy. Registered above `/{account_id}` on purpose. FastAPI matches in registration order and `options` is a valid UUID path as far as the router is concerned, so the reverse order sends every call here into the by-id handler and answers 422. See `tests/unit/api/test_route_order.py`. - [Get a virtual account](/docs/api/virtual-accounts/get_account) - [Stop an account collecting](/docs/api/virtual-accounts/disable_account): Money sent to a disabled account still arrives — the partner does not know it is off. It goes to suspense rather than to the business, and shows up in the ops queue with `account_disabled` against it. - [Resume collecting](/docs/api/virtual-accounts/enable_account) - Payouts: Sending money out. Raised in the dashboard or by an approver — an API key can read payouts but cannot create one. - [List payouts](/docs/api/payouts/list_payouts) - [Send a payout](/docs/api/payouts/create_payout): Raise a payout and commit the money to it. The money is reserved before this returns, whether or not the payout needs approval. A `requires_approval` response therefore means "the funds are held and somebody must sign", not "nothing has happened yet". In **test mode** the payout is settled as it is raised: no rail is involved, so the response already carries the outcome. `amount_minor` and the destination decide what that outcome is — see the sandbox guide for the values that produce a decline, a timeout, a return or a review hold. - [How this payout got to where it is](/docs/api/payouts/payout_timeline): Every transition in order, with timestamps and who caused each — E6-10. Ordered by when the event was written rather than by status, because a lifecycle read back in the order the code happens to define is not a record of what occurred. - [The limits this business is subject to](/docs/api/payouts/list_limits): Being refused by a limit nobody will show you is not a control, it is an outage. Setting them is not a business action — they are ours. Pass `currency` to get the whole picture: the tier's own ceiling is derived per currency rather than stored, so without one this can only return the rows an operator has written, and a business would be held to a limit this endpoint never mentioned. - [Quote the fee on a payout](/docs/api/payouts/quote_payout_fee): InversePay's fee on a payout, from your plan, before you send it. Priced exactly as the payout itself will be, so the quote is the fee. It is added on top, so the recipient receives `amount_minor` in full and `total_minor` leaves your balance. If you charge your own customers for sending money - a payroll run, a transfer - this is your cost per payout. What you charge them on top of it is your margin. A batch is priced line by line in the same way when its file is checked. - [Payouts held for review](/docs/api/payouts/parked_payouts): The compliance queue: payouts holding money and waiting on a human. - [Get a payout](/docs/api/payouts/get_payout) - [Clear a compliance hold](/docs/api/payouts/release_payout): Let a parked payout continue. It rejoins its normal path — if the business's thresholds want a signature it goes back to `requires_approval`. Clearing a compliance hold is not the same as approving the spend. - [Refuse a parked payout](/docs/api/payouts/reject_payout): Refuse it for good. The reservation goes back to the business. - [Cancel a payout](/docs/api/payouts/cancel_payout): Withdraw a payout that has not reached a rail. The reservation is released, so the money is spendable again immediately. - Payout batches - [List payout batches](/docs/api/payout-batches/list_batches) - [Create a batch from rows](/docs/api/payout-batches/create_batch): Validate every row and report what cannot run. Nothing is paid here. The response carries the per-line errors so a bad file is fixed before any money is committed. - [Create a batch from a CSV](/docs/api/payout-batches/upload_batch) - [Get a batch](/docs/api/payout-batches/get_batch) - [Approve a batch](/docs/api/payout-batches/approve_batch): Sign the batch and, once it has every signature, check the business can cover the run. A batch awaiting approval is signed through the approvals engine, the same rule as `POST /approvals/{id}/approve` (IN-362): the uploader is refused, and a policy needing more signatures leaves the batch awaiting approval, which the response's `status` says. Only a role that may decide approvals signs; anyone else is told the batch is still waiting. - [Run a batch](/docs/api/payout-batches/run_batch): Raise a payout per valid line. Safe to call again after a failure: each line's idempotency key is derived from the batch and its number, so a second run finds the payouts the first made rather than making them twice. - [Cancel a batch](/docs/api/payout-batches/cancel_batch) - Payout schedules - [List payout schedules](/docs/api/payout-schedules/list_schedules) - [Create a payout schedule](/docs/api/payout-schedules/create_schedule): Set up a recurring run. Nothing is validated against a rail here — the lines are checked when each occurrence materialises, so a beneficiary blocked after the schedule was created is caught on the run rather than being paid on stale information. - [Runs that produced no payment](/docs/api/payout-schedules/held_runs): Everything that was due and did not pay, with what it needed and what was there. - [Get a schedule](/docs/api/payout-schedules/get_schedule) - [Replace who gets paid](/docs/api/payout-schedules/update_lines): Applies from the next run. Past runs keep the batch they produced. - [Pause a schedule](/docs/api/payout-schedules/pause_schedule): Nothing materialises while paused, and the missed occurrences are not made up when it resumes. - [Resume](/docs/api/payout-schedules/resume_schedule): Picks up from the next occurrence, not from where it was paused. - [End a schedule](/docs/api/payout-schedules/end_schedule): Stops it for good, keeping its run history. - Payroll: Paying many people at once, each line routed on its own merits. - [List employees](/docs/api/payroll/list_employees) - [Add an employee](/docs/api/payroll/add_employee): Add somebody to the directory with the net amount they are paid. - [Change an employee's net pay](/docs/api/payroll/update_pay): Applies from the next run. Runs already made keep what they paid. - [Suspend, reinstate or offboard](/docs/api/payroll/set_status): Only active employees appear in a run. Offboarding keeps the record of what they were paid. - [List payroll runs](/docs/api/payroll/list_runs) - [Run payroll](/docs/api/payroll/run_payroll): Pay every active employee the net amount configured for them. Creates a batch that **always** waits for approval, whatever the business's thresholds say. Employees on wallets and employees on bank accounts go out in the same run, on the rail that suits each. Running the same period twice is refused. - [Get a payroll run](/docs/api/payroll/get_run) - Webhooks: Where we send events, how they are signed, and what happens when your endpoint is down. - [List webhook endpoints](/docs/api/webhooks/list_endpoints) - [Register a webhook endpoint](/docs/api/webhooks/register_endpoint): Add somewhere to send events. **The secret is shown once.** - [Issue a new signing secret](/docs/api/webhooks/rotate_secret): Issue a new signing secret, shown once — E11-1 AC2. By default the outgoing secret keeps signing for a day, so this can be done before the receiver is updated rather than in lockstep with it. A rotation that has to be coordinated is a rotation that does not happen. - [Stop sending to an endpoint](/docs/api/webhooks/disable_endpoint) - [Resume sending](/docs/api/webhooks/enable_endpoint): Clears the failure streak, so an endpoint fixed at the other end is not switched off again by its own history. - [Delivery attempts](/docs/api/webhooks/list_deliveries): What was sent, when, and what the receiver said — so "did you send it" has an answer that is not somebody's memory of a log. - [Events we stopped trying to deliver](/docs/api/webhooks/dead_letter): The queue to work through after an incident — E11-1. Oldest first. Each of these is an event a receiver never got, and every one of them can be replayed once whatever broke has been fixed. - [Try a dead-lettered delivery again](/docs/api/webhooks/replay_delivery): Give up on giving up. Needs `WEBHOOK_MANAGE`, not `WEBHOOK_READ`: this puts traffic back on a receiver, and the person doing it is asserting that whatever broke is fixed. - Beneficiaries: Saved recipients, with review and blocking. - [List beneficiaries](/docs/api/beneficiaries/list_beneficiaries) - [Save a beneficiary](/docs/api/beneficiaries/create_beneficiary): Save a recipient. The response carries the name-check result and the screening state, so a mismatch is visible immediately rather than at the first payout. - [Beneficiaries awaiting review](/docs/api/beneficiaries/review_queue): Everything a human must look at before money can move to it. - [Get a beneficiary](/docs/api/beneficiaries/get_beneficiary) - [Clear a sanctions hit after review](/docs/api/beneficiaries/clear_beneficiary): Judge a hit a false positive and let payouts through. The Owner's alone: it is the one action that turns "this may not be paid" into "this may". - [Confirm a sanctions hit](/docs/api/beneficiaries/block_beneficiary) - [Retire a beneficiary](/docs/api/beneficiaries/archive_beneficiary): Retire rather than delete: deleting takes the screening history with it and leaves past payouts pointing at nothing. - Bill payments - [Billers](/docs/api/bill-payments/biller_catalog): The same billers the consumer app sells, priced for businesses. - [Check an account](/docs/api/bill-payments/validate_account): Ask the biller who owns this meter, phone or smartcard. A mistyped digit costs a phone call when it is caught here and the money when it is not. A biller that cannot answer returns nothing rather than failing — most of them cannot, and refusing to pay them would be worse. - [List bill payments](/docs/api/bill-payments/list_bills) - [Pay a bill](/docs/api/bill-payments/create_bill): Pay a utility, airtime or tax bill from the business wallet. This raises a payout, so everything that governs a payout governs this: the funds are reserved, the approval thresholds apply, the per-tier and per-day limits apply, and the destination is screened. A bill above the threshold comes back `awaiting_approval` and pays nobody until it is signed. - [Get a bill payment](/docs/api/bill-payments/get_bill) - Statements: Reconciliation-grade statements. Opening plus entries equals closing, derived from the ledger rather than a report. - [A period of the ledger](/docs/api/statements/read_statement): Entries for a period, with the balances that bracket it — AC1. `start` is inclusive and `end` exclusive, so consecutive months tile: no entry lands in two statements or in neither. The opening and closing balances describe the whole period however many pages it is read in — a page-local pair would be a different number wearing the same name. - [Render a period to a file](/docs/api/statements/request_export): Queue a statement export — AC2. Always 202, never the file: a year of a busy corridor is hundreds of thousands of lines and rendering it inside the request is a gateway timeout for the caller and a held cursor for us. The `export.ready` webhook arrives when it is done, or poll this job. Asking twice for the same period returns the same job, so a dashboard that polls by re-POSTing costs one render rather than one per poll. - [Exports requested](/docs/api/statements/list_exports) - [One export's progress](/docs/api/statements/read_export) - [A short-lived link to the rendered file](/docs/api/statements/download_export): A signed URL, not the bytes. Streaming a hundred-megabyte CSV back through the API server would hold a worker for the length of somebody's download. The signed link expires in minutes: it is a bearer token for a financial document, and one that outlives the tab it was opened in is a liability. - Reports - [Export collections](/docs/api/reports/export_collections): Every collection in the period, whatever became of it. Failed and refunded charges are included rather than filtered out: a report showing only what settled answers "how much came in" and hides "how much did not", which is the half a business is trying to act on. - [Export payouts](/docs/api/reports/export_payouts) - [Export the audit trail](/docs/api/reports/export_audit): Every audited action in the period, for an auditor to hold. **Attempts as well as successes.** `succeeded` is a column, not a filter: a trail that records only what worked answers "what did we do" and not "what was attempted", and an investigation starts from the second. **Oldest first**, unlike the activity screen. Somebody reading an export is reconstructing a sequence; the newest-first order that suits a dashboard makes that read backwards. Actors are named, not just identified — the file is joined to the user rather than leaving an auditor to resolve UUIDs from a second export. System and API-key actions keep their place, with no name. - [Export one payroll run, per employee](/docs/api/reports/export_payroll): One row per employee, matching the batch outcome — including failures. Rows come from the batch lines, which exist whether or not a payout was created, so an employee whose line failed validation appears with the reason rather than vanishing from the report. Those are exactly the people who were not paid and whom somebody has to chase. - [Reports requested](/docs/api/reports/list_report_exports) - Notifications - [What we notify about](/docs/api/notifications/read_settings): Absent means on. An event with no row is enabled on both channels — a business that has never opened this screen should still hear that a payout failed, so the useful default cannot require a row to exist. - [Change a preference](/docs/api/notifications/write_setting): Set one event's channels, for the business or for yourself. Anyone may silence their own mail. Changing what the *business* hears is `NOTIFICATION_MANAGE`, because switching off payout failures for everybody is a decision with consequences for people who did not make it. - [Your notifications](/docs/api/notifications/read_inbox): Scoped to the caller, always. There is no parameter for whose inbox to read. A notification is addressed to a person, and an endpoint that could be pointed at somebody else's would hand any member the finance team's mail. - [Mark notifications read](/docs/api/notifications/mark_read) - Metrics - [How long the money lasts at the recent rate of burn](/docs/api/metrics/read_runway): Available balance over the recent burn, per currency. **Net burn, not gross outgoings.** A business that collects as well as pays out is burning the difference. Dividing the balance by `money_out` would understate the runway of most businesses on this platform, and in the reassuring direction is the only direction this figure must not be wrong in. **Computed here rather than in the client**, which could otherwise do the arithmetic itself: a client reading a capped statement gets a truncated window, an understated burn, and an overstated runway. "Fourteen months" when it is four. Per currency and never summed — adding EUR to USD gives a number of nothing. `/fx/display/balance` is the endpoint that converts. - [This period against the last](/docs/api/metrics/read_kpis): Money in, money out, net and closing balance, with the period before. The comparison window is the same length immediately before, not the previous calendar month. Comparing a partial month against a whole one shows a fall that is an artefact of the calendar, which is the classic way a dashboard invents bad news. Reported per currency and never summed. Adding 500 EUR to 500 USD gives 1000 of nothing; `/fx/display/balance` is the endpoint that converts, and it says when it could not price a leg. - [A closing balance per day](/docs/api/metrics/read_balance_history): What this wallet held at the end of each day, oldest first. Each point is the `balance_after` of the last entry on or before that day, which is the ledger's own running total — so a sparkline drawn from this agrees with the statement rather than approximating it. A day with no movement repeats the previous close, because that is what the balance was. Days before the wallet held anything are omitted rather than reported as zero: a leading zero draws a rise out of an account that did not exist. - [How the currencies you hold have moved](/docs/api/metrics/read_fx_movement): Each held currency against the display currency, then and now. Both ends are the newest rate at or before their moment, so a weekend with no quotes compares against Friday rather than reporting nothing. A pair with no rate at either end comes back with a null move. That is not the same as no movement, and a client should render it as a dash — reading an absent rate as 0% is how a screen tells somebody a currency is stable when nobody has priced it in a week. Reference rates only. `settleable` is always false, and `/fx/quotes` is where a trade is priced. - F X: Quoting, locking and executing a conversion at a disclosed rate. - [Recent quotes](/docs/api/fx/list_quotes) - [Lock a conversion rate](/docs/api/fx/create_quote): Price a conversion and hold it until `expires_at`. Quoting needs only read permission — being told a price commits nobody. Executing needs `fx:execute`, because that moves money. 503 rather than 500 when nothing will quote the pair: no registered provider would deal it, which is a condition that clears by itself and is worth retrying. E10-11's rule stands — we do not fall back to a rate nobody offered. - [One quote](/docs/api/fx/read_quote) - [Convert at the locked rate](/docs/api/fx/execute_quote): Post both legs at exactly the disclosed figures — AC1. 409 for a spent quote and 410 for an expired one, because the two need different things from the caller: one has already happened and must not be repeated, the other needs a fresh price. - [Release a lock](/docs/api/fx/cancel_quote) - [Rate alerts you have set](/docs/api/fx/list_alerts): Every rate alert this business has set, newest first. `last_triggered_at` is null on an alert that has not fired yet, which is the ordinary state of a useful one rather than a sign anything is wrong. - [Set a rate alert](/docs/api/fx/create_alert): Watch a pair and notify when it crosses `rate`. 409 when the condition already holds. An alert fires on the rate *crossing* the threshold rather than on it being past it, so one set above a rate that is already higher could not fire until the rate first fell back — storing it would hand somebody a control that looks armed and is not. The error says what the rate is now, which is the thing they were asking about. 409 too when the same threshold is already set on the same pair: that is a double-click rather than a second intention. - [Remove a rate alert](/docs/api/fx/delete_alert): Stop watching a threshold. Deleting rather than deactivating: an alert is a small, cheap thing to set again, and a list cluttered with switched-off rows is one nobody reads. The notifications it already sent are unaffected — they are a record of what the rate did, not of what is being watched now. - F X display - [Display currency](/docs/api/fx-display/read_preference) - [Change it](/docs/api/fx-display/set_preference): Change what totals are shown in. This moves no money and converts nothing. It does not decide which currencies the business may hold, what a payout settles in, or what a conversion costs — it changes a heading. That is why it is a settings permission and not `fx:execute`. - [Balances as one figure](/docs/api/fx-display/display_balance): Every balance this business holds, added up in one currency. A heading, not a quote. The rates behind it are usually reference rates that no counterparty would deal at, which is fine for a number on a screen and disqualifying for a trade — `may_settle` says which you have. - [Add up arbitrary amounts](/docs/api/fx-display/display_total): For a screen that has already fetched its own figures. Minor units in and minor units out. A total assembled from floats disagrees with the balances it was built from, and somebody eventually reconciles the two. - [The rates a screen would use](/docs/api/fx-display/display_rates): So a dashboard's per-row conversions agree with its heading. `null` for a pair nothing can price — the same honesty as leaving a currency out of a total, in the shape a front end wants. - Cards: Issuing cards, and the limits and holds that govern them. - [Cards](/docs/api/cards/list_cards) - [Issue](/docs/api/cards/issue_card): Request a card. It cannot spend until the provider issues it. At least one limit is required. A card with none is a standing instruction to move a business's money without a ceiling, and the absence of a decision is not the same as deciding to allow everything. - [One card](/docs/api/cards/read_card) - [Change the limits](/docs/api/cards/set_limits): Takes effect on the next authorisation. Deliberately not retroactive: lowering a limit below what has already been spent today refuses the next spend rather than clawing anything back. Anything else would be reversing a purchase somebody has made. - [Change where the card may be used](/docs/api/cards/set_controls): Category and country rules, separate from the amount ceilings. Takes effect on the next authorisation, like the limits, and for the same reason: a rule added now does not reverse a purchase already made. A refusal here is reported as its own decline reason — `category_not_allowed` or `country_not_allowed` — rather than as a limit breach, because the two lead to different actions. Somebody over a limit waits or asks for a raise; somebody blocked by category is holding the wrong card. **A missing value passes.** Not every network sends a category or a country on every authorisation, and refusing on absence would decline ordinary spend for a field the acquirer chose not to populate. - [Room left](/docs/api/cards/remaining): What a cardholder wants before they try, and what a support agent needs when they ask why not. - [Stop it, for now](/docs/api/cards/freeze_card): The reason is required: a frozen card is a person unable to buy something, and "it just stopped working" is the support call. - [Start it again](/docs/api/cards/unfreeze_card) - [Off for good](/docs/api/cards/cancel_card): The row stays. Its spend is part of the book. - [Everything asked of this card](/docs/api/cards/list_authorizations): Approved and refused alike — AC1's "logs a card.declined event" is also a row here, because a decline nobody can look up is a cardholder we cannot answer. - Groups - [Create a group](/docs/api/groups/create_group): Create a reporting group and put the current business in it. A group is a lens, not a container: it holds no money, verifies nothing, and grants nobody access to anything. Each entity keeps its own KYB, tier and wallet. - [Put the current business in a group](/docs/api/groups/add_current_business): Adds **the business you are acting for**, not one named in the body. Naming another business here would let somebody file an entity they have no rights over into a group of their choosing. Joining is done from inside the entity joining, by somebody who can already manage its settings. - [Group position](/docs/api/groups/group_summary): What the group holds, across the entities **you** can already see. A group grants no access. Entities you are not a member of are counted in `hidden` and not described — otherwise filing two companies under one name would quietly let one team read the other's balance. Entities still in KYB have no wallet and appear in `excluded` with the reason, rather than being dropped from the sum. A partial figure presented as a total looks correct, which is what makes it worse than an error. - [Changelog](/docs/changelog): What changed, when, and what you have to do about it. - [Deprecation policy](/docs/deprecation): How long you get, and how you find out.