/ Payouts v1 Dashboard ↗

Security

How the partner API is secured, what each control actually enforces, and what we deliberately do not do. If you are filling in a security questionnaire, the vocabulary table at the end maps the usual terms onto what we call them.

The credential#

One header, one secret. The complete API key is the credential — there is nothing to sign and no token exchange:

bash
curl https://api.avvio.xyz/business/api/v1/payments/organizations/{orgId}/balance \
  -H "x-api-key: avvio_live_…"

We store a SHA-256 hash of the complete key and its public lookup prefix. The key itself is never persisted and never logged, which is why we show it once at issuance and cannot show it again. SHA-256 rather than a password KDF is deliberate: the secret is 256 random bits, so there is no dictionary to slow down.

A key is bound to one organization and is refused on any other's routes.

Limiting what a leaked key can do#

Three settings are fixed when a key is issued. All three are enforced on every request, server-side, and all three survive rotation.

Control Also called What it enforces
Scopesread / write Scoped tokens, least privilege A key scoped read is refused any method but GET, and refused full account details even on a GET
Allowed source addresses IP allowlisting, IP pinning A request from any other address is refused with KEY_IP_NOT_ALLOWED, not silently permitted
Expiry Credential lifetime 365 days by default, 730 at most. The key dies without anyone having to chase it

Scopes are coarse on purpose: a key either reads or it writes. There is no per-resource or per-route scope — no "may create payouts but not touch beneficiaries". A permission matrix is a thing to get subtly wrong, and "reads only" is a promise you can verify yourself by reading the HTTP method. If you need a finer boundary today, the boundary is a separate organization, not a separate scope.

A read-only key is the cheapest exposure reduction available to you, because reconciliation jobs, monitoring probes and dashboards are exactly where a credential tends to spread.

Scopes are not the outer boundary — the route allowlist is. A key authenticates as a machine member of your organization, so the guard is default-deny: a perfectly valid key is refused on every route that has not explicitly opted in, and refused before we so much as look it up. Only the payout surface is opted in. Team management, KYB documents, provider terms and wallet operations are not reachable with a key at all, whatever its scope — so the question "what could this credential touch?" is answered by the endpoint list in this documentation, not by a permission matrix you have to reason about.

Source addresses must be exact. A CIDR block is not matched, so a key pinned to one would refuse every request; issuance rejects a range rather than letting you find out in production.

All three are fixed at issuance. There is no endpoint that edits a live key's scopes, addresses or expiry, and rotation carries them forward unchanged rather than reopening them. Changing any of them means revoking the key and issuing a new one — so decide them when you issue, and issue a second key rather than widening the first.

Rotation#

POST /api-keys/{id}/rotate mints a successor and gives the predecessor a deadline — 24 hours by default — rather than killing it immediately. Both work during the overlap, so there is no window where neither does. Rotation that causes an outage is rotation nobody performs.

The successor carries the predecessor's scopes, allowed addresses and lifetime. Rotation renews a credential; it never widens one. A read-only key cannot become a spending key by being rotated, and a key you deliberately issued for thirty days comes back as another thirty-day key rather than a yearly one.

Lifetime, not remaining time: the successor starts its predecessor's full span again. A successor inheriting only what was left would shrink on every rotation until a key expired inside its own overlap window.

Three things rotation deliberately refuses:

Issuing, rotating and revoking are human actions in the dashboard. A key cannot mint, list, or revoke another key — including itself. A stolen credential cannot be used to manufacture more.

And a key cannot approve its own spend. The machine member a key authenticates as holds the operator role: inside the set allowed to *propose* money movement, and deliberately outside the set of treasury signers. Where your organization requires M-of-N approval on a payment, the approving signatures come from owner and admin members holding their own passkeys — a credential cannot be both the thing that requests a payment and one of the things that authorizes it. Accepting provider terms and changing team membership sit outside the same boundary.

Environment separation#

avvio_test_ and avvio_live_ are separate rows resolving to separate organizations. The environment is read from the credential's own prefix, never from a database column, so a stale row can never promote a test key to live money. A test key addresses the same organization id as your live key and resolves server-side to that organization's sandbox — the only difference between staging and production is the value of one variable.

Revocation and audit#

Revocation is immediate. Every key carries an event trail — issued, rotated, revoked — with the actor responsible and, on a rotation, the successor's id. Expiry is passive and records no event: the key simply stops working when its expiresAt passes.

If a key may have been exposed, revoke it in the dashboard and issue a successor, then tell us at security@avvio.xyz. We would rather hear about a false alarm than find out later.

Data protection#

The API is served over HTTPS.

Personal data is encrypted at rest at the field level, not merely on an encrypted volume. Each protected field is sealed with AES-256-GCM under a data key wrapped by AWS KMS, and the ciphertext is bound with additional authenticated data to the row that owns it — so a ciphertext lifted from one record does not decrypt against another.

What that covers on your integration:

Encrypted Holds
RecipientPaymentMethod.details The full destination account — CLABE, IBAN, account and routing numbers
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

This table is the whole of it, and the boundary matters. Field-level encryption covers the fields listed above, not every field in our database. Data we receive back from a payment network — the account record it returns after we register a beneficiary, for instance — is retained as that network sent it. Ordinary contact fields on your organization and its members are stored as written. If a specific field matters for your review, ask and we will tell you which side of the line it is on rather than leaving you to infer it from a table.

Two consequences worth knowing. Listings return a last4 rather than the full account, which is what reconciliation actually needs; reading the complete details is a separate call that a read-only key is refused. And secrets we mint for you are returned exactly once at creation and are never retrievable through the API afterwards — but for different reasons, and the difference is worth stating.

The API key we genuinely cannot return: we store only a SHA-256 hash, so there is nothing to give back. The webhook signing secret we can read, because signing your deliveries requires it; what protects it is that no endpoint returns it and no key can ask. If your threat model distinguishes "cannot be disclosed" from "is not disclosed", those two secrets sit on different sides of that line.

Webhook authenticity#

Deliveries are signed with Standard Webhooks (HMAC-SHA256), so you can verify with an off-the-shelf library rather than against a format only we speak. Verify over the raw bytes before parsing, and dedupe on svix-id. See Webhooks.

A webhook is a nudge, never the record. Build reconciliation against the API.

Where we will deliver. An endpoint URL must be HTTPS and must not carry credentials in the URL. The destination address is resolved when the endpoint is registered and pinned for the connection itself, so a hostname that resolves one way for the safety check and another way at connect time cannot be used to point a delivery somewhere it was not allowed. Deliveries time out after 10 seconds. Plain HTTP is accepted only against literal loopback, only in the sandbox, so your first receiver can be a script on your laptop.

The signature header carries a space-separated list of values, so a secret rotation can present two valid signatures on one request — verify against any match rather than assuming a single value.

Dashboard access#

Issuing, rotating and revoking keys are human actions, so the dashboard is part of your attack surface and is worth stating.

Sign-in is passwordless: authentication is a passkey (WebAuthn), held in your authenticator and bound to the origin. There is no password to phish, reuse, or find in a breach dump, and no password reset flow to social-engineer. A team member cannot be added without registering one.

Membership carries a roleowner, admin, operator or viewer — enforced server-side on the routes that declare one, not in the interface.

A dashboard session is not a bearer token. A stolen session alone is not enough to move money or touch a credential: requests that mutate are signed in the browser with the session's own key, over a canonical form of the request — method, path, body, idempotency key, timestamp and a nonce — and we verify that signature server-side. The timestamp must be fresh and the nonce is single-use, enforced by a uniqueness constraint rather than a cache, so a captured request cannot be replayed even within its freshness window.

This is the one place we do require request signing, and it is why the partner API not requiring it is a deliberate trade rather than an oversight: a browser can hold a signing key in an authenticator, and a partner's server-to-server integration would have had to build one.

Rate limiting#

100 requests per minute per credential by default, 600 on high-volume payout and reconciliation routes, and a 2,000-per-minute source-address ceiling that always applies regardless of the credential presented. Limits and Retry-After arrive in the response headers — read them rather than guessing. See Going Live.

What we do not do#

Stated plainly, because a security review should not have to infer it from silence:

Authentication and authorization

Key lifecycle

Webhooks

Organization and audit

The bearer model is the same shape as Stripe, Increase and Mercury, and it is chosen for the same reason: every HTTP client, SDK and test tool works with it unmodified. If your risk assessment needs one of the above, raise it with us before you integrate rather than after.

Your side of the boundary#

The controls above bound what a leaked key can do. They do not stop it leaking.

Reporting a vulnerability#

Email security@avvio.xyz. Please do not open a public issue or disclose publicly until a fix has shipped. We acknowledge within 2 business days and give an initial severity assessment within 5, and we will credit you when the fix ships unless you would rather we did not.

Out of scope: anything requiring a compromised API key — that is the credential working as designed, not a flaw.

If you are filling in a security questionnaire#

The docs describe these controls in integration language. Questionnaires ask for them in another vocabulary; this is the mapping.

The questionnaire asks about We call it Where
OAuth 2.0 / OIDC Not offered — bearer API key Above
Scoped tokens, least privilege read / write scopes Above
IP allowlisting / whitelisting Allowed source addresses, IP pinning Above
API key rotation Rotation with overlap Above
Credential storage at rest SHA-256 hash, shown once Above
Encryption at rest Field-level AES-256-GCM under KMS Above
Encryption in transit HTTPS, and HTTPS required of your webhook endpoint Above
Webhook authenticity Standard Webhooks HMAC-SHA256 Webhooks
Your egress addresses, for our allowlist Not published — verify the signature instead Above
SSO / SAML / SCIM Not offered — passkeys and manual membership Above
MFA on admin accounts Passwordless passkeys (WebAuthn) Above
Role-based access control owner / admin / operator / viewer Above
Audit logging Key event trail, recorded but not exposed as an API Above
Rate limiting / abuse controls Per-credential and per-address limits Going Live
Vulnerability disclosure security@avvio.xyz Above