Security

The security model: credential storage, scopes, allowed source addresses, rotation, data protection, and what we deliberately do not do.

📘

In plain English

Your API key can move money, so this page is about limiting what a stolen copy could do. Three settings do most of the work — read-only, allowed source addresses, and expiry — and rotation lets you replace a key without an outage. Personal data is encrypted field by field, not just on an encrypted disk. The last sections say plainly what we do not do, so a security review does not have to guess.

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:

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.

See Authentication for the format and how to get one.

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.

ControlAlso calledWhat it enforces
Scopesread / writeScoped tokens, least privilegeA key scoped read is refused any method but GET, and refused full account details even on a GET
Allowed source addressesIP allowlisting, IP pinningA request from any other address is refused with KEY_IP_NOT_ALLOWED, not silently permitted
ExpiryCredential lifetime365 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 and allowed addresses.
Rotation renews a credential; it never widens one. A read-only key cannot become
a spending key by being rotated.

Three things rotation deliberately refuses:

  • An expired key. Otherwise "rotate" means "resurrect" — the dead key would
    come back for the whole overlap window, which is the state expiry exists to end.
  • A revoked key. Same reason.
  • A second successor. One successor per predecessor, so a double-click or a
    retry after a timeout returns KEY_ALREADY_ROTATED and names the successor
    that already exists, instead of quietly minting a second live credential.

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 [email protected]. 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:

EncryptedHolds
RecipientPaymentMethod.detailsThe full destination account — CLABE, IBAN, account and routing numbers
KYB associated personsTax id, government id number, date of birth, email, phone, address
KYC profilesTax id, phone, address
Custom bank accountsAccount number, ABA routing number, nostro account

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 — the API key, the webhook signing secret — are returned exactly once at
creation and are not retrievable afterwards, because we do not keep them in a
readable form.

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

  • No OAuth 2.0, and no OpenID Connect. This is a server-to-server API with
    no end-user consent step to model and no third party acting on a user's
    behalf, which is the problem OAuth exists to solve. Authentication is the API
    key described above. There is no authorization code flow, no client
    credentials grant, and no bearer token endpoint.
  • No request signing and no mTLS on the partner API. The API key is a bearer credential: anyone
    holding it can use it, from anywhere the allowed-address list permits. A
    request altered in flight by a proxy you control is not detectable by us. This is a statement about the partner API only — the dashboard does prove possession, described under Dashboard access.
  • No per-resource scopes. read and write are the whole set. There is no
    key that may create payouts but not read beneficiaries.

Key lifecycle

  • No editing a key in place. Scopes, allowed addresses and expiry are fixed
    at issuance; changing one means revoke and re-issue. Rotation deliberately
    carries them forward rather than reopening them.
  • No API-driven issuance. Keys are minted, rotated and revoked by a human in
    the dashboard. A key cannot mint another key, which also means you cannot
    automate rotation end to end — the rotate call itself is dashboard-authenticated.

Webhooks

  • No self-serve signing-secret rotation. The signature format supports an
    overlap, but there is no endpoint that rotates a live endpoint's secret. To
    replace one, register a new endpoint and remove the old one.
  • No published egress addresses. We do not publish the IP ranges our
    deliveries originate from, so you cannot allowlist us inbound by address.
    Authenticate deliveries by their signature, which is what it is for.

Organization and audit

  • No SSO, SAML or SCIM. Dashboard access is per-user passkeys and manual
    membership; there is no directory integration and no automated deprovisioning.
  • No audit-log API. The key event trail described above is recorded, but it
    is not exposed as a partner-facing endpoint — you cannot pull it into your own
    SIEM today. Ask us and we can retrieve it.

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.

  • Store the key in a secrets vault, or an environment variable if you have no
    vault. Never in source control, a client-side bundle, a mobile app, a
    screenshot, or a support ticket.
  • Server-side only. Use a test key in the documentation console if you want
    to experiment; never put a test or live key in your own browser bundle.
  • Use a separate key per service — not because they have different powers, but
    so revoking one does not take down the others.
  • Use a read-only key wherever the job only observes.

Reporting a vulnerability

Email [email protected]. 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 aboutWe call itWhere
OAuth 2.0 / OIDCNot offered — bearer API keyAbove
Scoped tokens, least privilegeread / write scopesAbove
IP allowlisting / whitelistingAllowed source addresses, IP pinningAbove
API key rotationRotation with overlapAbove
Credential storage at restSHA-256 hash, shown onceAbove
Encryption at restField-level AES-256-GCM under KMSAbove
Encryption in transitHTTPS, and HTTPS required of your webhook endpointAbove
Webhook authenticityStandard Webhooks HMAC-SHA256Webhooks
Your egress addresses, for our allowlistNot published — verify the signature insteadAbove
SSO / SAML / SCIMNot offered — passkeys and manual membershipAbove
MFA on admin accountsPasswordless passkeys (WebAuthn)Above
Role-based access controlowner / admin / operator / viewerAbove
Audit loggingKey event trail, recorded but not exposed as an APIAbove
Rate limiting / abuse controlsPer-credential and per-address limitsGoing Live
Vulnerability disclosure[email protected]Above

Did this page help you?