---
updatedAt: 2026-09-30T15:54:20.000Z
---

Fetch the complete documentation index at: https://docs.avvio.xyz/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Security

How the partner API is secured and what each control enforces. Day-to-day key
handling is in [Authentication](/authentication/); a
[questionnaire map](#if-you-are-filling-in-a-security-questionnaire) is at the
end.

## The credential

The complete API key, sent as `x-api-key`, is the credential. We store only a
SHA-256 hash and the public lookup prefix, and never persist or log the key,
so it is shown once. SHA-256 needs no password KDF because the secret is 256
random bits.

A key is bound to one organization. `avvio_test_` and `avvio_live_` keys
resolve to separate organizations, and the environment comes from the key's
prefix, never a database column, so a stale row cannot promote a test key to
live money.

## Limiting what a leaked key can do

Three settings are fixed at issue, enforced server-side on every request, and
carried unchanged through rotation. No endpoint edits them, so issue a second
key rather than widening the first.

| Control | Also called | What it enforces |
| --- | --- | --- |
| Scopes: `read` / `write`, plus the consents `crypto_payouts` and `refunds` | Scoped tokens, least privilege | A `read` key is refused any method but `GET`, and refused full account details even on a `GET`. A consent is off until granted; `refunds` can be granted only by an owner or admin |
| Allowed source addresses | IP allowlisting, IP pinning | A request from any other address is refused with `KEY_IP_NOT_ALLOWED` |
| Expiry | Credential lifetime | 365 days by default, 730 at most |

There are no per-resource scopes, because a permission matrix is easy to get
subtly wrong and "reads only" can be verified from the HTTP method. The two
consents come only with `write`: `crypto_payouts` spends from your own wallet,
and `refunds` returns a buyer's money. For a finer boundary, use a separate
organization. Source addresses must be exact; issuance rejects a CIDR block.

A key authenticates as a machine member of your organization behind a
default-deny route allowlist, which only the payout surface has joined. Team
management, KYB documents, provider terms, wallet operations and key
management are unreachable, so a stolen key cannot mint more. The
[API reference](/reference/) is the complete list of what a key can touch.

## Rotation and revocation

Rotation (`POST /organizations/{orgId}/api-keys/{apiKeyId}/rotate`, from a
signed-in dashboard session only; an API key cannot call it) mints a successor and gives the predecessor a
deadline, 24 hours by default; both work during the overlap. The successor
keeps the scopes and addresses, so rotation cannot turn a read-only key into a
spending key, and gets a full lifetime, so rotation never shrinks a key into
its own overlap window.

Rotation refuses an expired or revoked key, which it would otherwise revive.
A second rotation, such as a retry after a timeout, returns
`KEY_ALREADY_ROTATED` with the existing successor's id.

Issuing, rotating and revoking happen in the dashboard, so rotation cannot be
automated end to end. Revocation is immediate. Each key's event trail
(`issued`, `rotated`, `revoked`) names the actor and any successor; expiry
records no event. If a key may be exposed, revoke it, issue a successor, and
tell **security@avvio.xyz**.

## Payout controls

A key's machine member holds the `operator` role, which may propose money
movement but cannot sign a treasury transfer; `owner` and `admin` members sign
on their own passkeys.

Above an amount threshold, your organization can require M-of-N approval on
API payouts. `POST /payouts` and batch `confirm` then answer `202` with an
approval, a batch holds at `awaiting_confirmation` whatever `autoCommit` said,
and nothing is priced or sent until `owner` or `admin` members reach quorum in
the dashboard. Votes need a human session and the initiator cannot approve
their own request, so a leaked `write` key can only propose
([Approvals](/status/#approvals)).

Every payout path is gated server-side: `POST /payouts`, batch `confirm`,
quote-and-accept, and payout links at mint and submit. With a threshold or cap
on, quote-and-accept answers `403 USE_POST_PAYOUTS`, so `POST /payouts` is the
one door for API calls. Payout links answer `USE_POST_PAYOUTS` only when the
amount would need approval, and enforce caps at submit
(`422 PAYOUT_LIMIT_EXCEEDED`).

| Control | On | What it enforces |
| --- | --- | --- |
| Route allowlist, `write` scope, `operator` role | Always | Required by the payout routes |
| `Idempotency-Key` | Always | Replays a retry; a 15-minute detector catches the same body under a new key ([Idempotency](/idempotency/)) |
| Source addresses, expiry | Always | [Per key](#limiting-what-a-leaked-key-can-do) |
| Approvals | With a threshold | M-of-N human approval |
| Velocity caps | When we set them, at your request | Per payout, per day and per end user per day, in USD, on USD-sourced payouts only today. `422 PAYOUT_LIMIT_EXCEEDED`; nothing is sent |
| Screening | Always | Destination-country list; sanctions and name screening [delegated](#what-we-do-not-do). `422 PAYOUT_REFUSED`, final |
| Purpose of payment | Always | `purposeOfPayment` required for INR, GHS, CNY and BRL, validated against the corridor's catalog, never defaulted |

## Audit trail

`GET /payments/organizations/{orgId}/audit-events` records every audited
mutation, succeeded or refused: payouts created or canceled; batches
submitted, confirmed or canceled; approvals decided; recipients created,
changed or deleted; keys issued, rotated or revoked; webhook endpoints created,
deleted, paused, re-signed or replayed. Each row carries the key prefix or
dashboard user, the source IP, the outcome, the error `type` on a refusal, and
the `requestId` that matches `x-request-id` and error bodies. The API never
returns a key's internal id, and a row holds no request body or bank field.

Results are newest first, cursor on `id`, filterable by `action`,
`resourceId`, `apiKey` (prefix), `actorUserId`, `createdAfter` and
`createdBefore`. A read-only key can read it, so your SIEM needs no credential
that moves money. A row is written after the request settles, outside the
money transaction; the payout and its event prove the movement.

## Data protection

The API is served over HTTPS. Personal data is encrypted at rest field by
field with AES-256-GCM, under a data key wrapped by AWS KMS, and bound by
additional authenticated data to its row, so a copied ciphertext does not
decrypt against another record.

| Encrypted | Holds |
| --- | --- |
| Recipient bank details | The full destination account: CLABE, IBAN, account and routing numbers |
| Bank details on invoices and checkout links | Account number, routing number, nostro account |
| KYB documents, reviews and research | Fields read from uploaded documents (government id number, date of birth, full name, tax id, address), review findings with their evidence, and public-register research on officers and beneficial owners |
| KYB associated persons | Tax id, government id number, date of birth, email, phone, address |
| KYC profiles | Tax id, phone, address |
| Custom bank accounts | Account number, ABA routing number, nostro account |

Records a payment network sends back, and contact fields on your organization
and its members, are stored as received; ask about any field. Listings return
`last4`; full details are a separate call a read-only key is refused. The API
key is never returned after creation. A webhook signing secret is returned
once, when the endpoint is created and when its secret is rotated, and never
again.

| Data | Kept | Then |
| --- | --- | --- |
| Batch line `instruction` (items and CSV export) | 90 days after the batch ends | `null`; status, errors and `payoutId` stay |
| Payout link sender (`endUser.name`, `endUser.email`) | Until the receipt email is sent, or the link expires unused | Removed from the link; the address the receipt went to is kept as the delivery record |
| Webhook delivery log | 30 days after delivery or exhaustion; pending retries kept | Deleted |
| `Idempotency-Key` records | 7 days; unresolved ones until support resolves them | Deleted |
| Recipients | Until you delete them | Soft-deleted, unpayable ([Recipients](/recipients/#correcting-and-removing)) |
| A recipient's single payment method | Until you delete it | Deleted (the row is erased) |
| Payouts and their events | Retained | Never deleted through the API |
| Audit events | At least five years (BSA record-keeping) | Nothing deletes them today |

## Webhook authenticity

Deliveries are signed with [Standard Webhooks](https://www.standardwebhooks.com/)
(HMAC-SHA256); [Webhooks](/webhooks/) covers verification. An endpoint URL
must be HTTPS without credentials. Its address is resolved at registration and
pinned for the connection, so a hostname cannot pass the check with one
address and connect to another. Deliveries time out after 10 seconds.

## Dashboard access

Keys are managed in the dashboard, so it is part of your attack surface.
Members sign in with a passkey (WebAuthn) bound to the origin, with no password
to phish; signup verifies the email with a one-time code.
Roles (`owner`, `admin`, `operator`, `viewer`) are enforced server-side.

The dashboard signs money-moving requests in the browser with the session's
own key, over the method, path, body, idempotency key, timestamp and nonce.
The server requires a fresh timestamp and a single-use nonce, held by a
uniqueness constraint. The partner API never asks for a signature.

Rate limits apply per credential and per source address; the numbers are in
[Environments](/environments/#rate-limits).

## What we do not do

- No OAuth 2.0 or OpenID Connect: there is no end-user consent step to solve.
- No request signing or mTLS on the partner API. Anyone holding the key can use
  it from any allowed address, and we cannot detect a request altered by a
  proxy you control.
- No per-resource scopes, no editing a key in place, and no API-driven
  issuance or rotation.
- No published egress addresses; verify our deliveries by signature.
- No SSO, SAML or SCIM, so no directory integration or automated
  deprovisioning.
- No streaming audit export; poll the [audit trail](#audit-trail).
- No name screening of our own. It is delegated in writing to the payment
  network; ask for the delegation letter during your review.

We use a bearer key because every HTTP client supports it. If your risk
assessment needs one of the above, raise it before you integrate. Keeping the
key from leaking is on your side:
[Authentication](/authentication/#treat-the-key-like-a-database-password)
has the rules.

## Reporting a vulnerability

Email **security@avvio.xyz** and do not disclose until a fix has shipped. We
acknowledge within 2 business days, assess severity within 5, and credit you
when the fix ships unless you ask us not to. Anything that requires a
compromised API key is out of scope.

## If you are filling in a security questionnaire

| The questionnaire asks about | Answer |
| --- | --- |
| OAuth 2.0 / OIDC, SSO / SAML / SCIM | [Not offered](#what-we-do-not-do) |
| Scoped tokens, IP allowlisting | [Scopes and allowed addresses](#limiting-what-a-leaked-key-can-do) |
| API key rotation | [Rotation with overlap](#rotation-and-revocation) |
| Credential storage at rest | [SHA-256 hash](#the-credential) |
| Encryption at rest and in transit | [Field-level AES-256-GCM, HTTPS](#data-protection) |
| Webhook authenticity | [HMAC-SHA256](#webhook-authenticity) |
| Our egress addresses | [Not published](#what-we-do-not-do) |
| MFA, role-based access | [Passkeys and four roles](#dashboard-access) |
| Audit logging | [Audit API, kept at least five years](#audit-trail) |
| Rate limiting | [Environments](/environments/#rate-limits) |
| Vulnerability disclosure | [security@avvio.xyz](#reporting-a-vulnerability) |
