Authentication
SorenPay uses workspace-scoped API keys prefixed apk_. Every authenticated
request carries one in the Authorization: Bearer … header.
Header format
curl https://sorenpay.com/api/treasury \
-H "authorization: Bearer apk_<your key>"Token lifecycle
| Action | Where |
|---|---|
| Create | In a sandbox workspace: Dashboard → Developers → Create key (a fixed set of scopes, see the quickstart). With the API: POST /api/auth/keys with a key that has write:api_keys and every scope it hands out |
| Reveal | Once on creation. Store immediately. |
| List | GET /api/auth/keys |
| Rotate | Create a new key → switch traffic → revoke the old one |
| Revoke | DELETE /api/auth/keys?id=… with a key that holds write:api_keys and every scope of the key it revokes (also revokes every key that key created). Revoking from the dashboard isn't available yet |
The full token is shown only at creation. If you lose it, you must create a new key and rotate.
Scopes
Every key carries an explicit list of scopes, and every endpoint checks the
one it needs (403 missing_scope:<scope> otherwise). * can no longer be
issued (keys minted before that still work until revoked — rotate them), and a
key with no scopes can do nothing. A write: scope does not imply the matching
read: scope: a key that sends payouts and then reads their status needs both
write:payouts and read:treasury. Scope each key to its exact role — an
agent's key typically needs read:agents + write:agents, plus read:cards
if it inspects its cards.
| Scope | What it grants |
|---|---|
read:treasury | Balances, ledger, recipients, payouts, exchange rates, USDC ramp orders |
write:treasury | Test deposits; multi-currency accounts and conversions |
write:counterparties | Save and remove payout recipients |
write:payouts | Send payouts, firm exchange-rate quotes, release or cancel held wires |
write:ramps | USDC quotes, orders, withdrawals and wallets (an old * key isn't enough) |
read:cards | List cards + lookup by id, list cardholders (DOB / SSN last-4 are never returned), physical cards |
write:cards | Mint, update limits, freeze + terminate cards; register cardholders |
read:agents | List agents + view intent log |
write:agents | Register agents + submit intents |
read:checkout | View checkout sessions |
write:checkout | Create checkout sessions |
read:api_keys | List this workspace's keys |
write:api_keys | Create/revoke other keys |
read:webhooks / write:webhooks | Reserved for outbound webhook endpoints (no endpoint uses them yet) |
Rate limits
Each key can make 100 requests a minute. All keys in a workspace share
600 a minute, and each IP address gets 1,200 a minute across the whole
API. Go over and you get HTTP 429 with a Retry-After header (in seconds) and
x-ratelimit-reset-ms. Wait that long, then retry.
If we briefly can't check limits, requests that change something get HTTP 503
rate_limit_unavailable with Retry-After. Nothing was changed, so retry with
the same Idempotency-Key.
For higher limits, contact us.
Idempotency
Routes marked idempotent in the API reference take an
Idempotency-Key header (payouts, recipients and quotes require it). Use a
UUIDv4:
curl -X POST https://sorenpay.com/api/cards \
-H "authorization: Bearer apk_<your key>" \
-H "idempotency-key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{...}'Replays within 24h return the cached response. Conflicting bodies on the same
key return HTTP 422 idempotency_key_mismatch so you can detect bugs.
Business verification (KYB)
Live mode isn't switched on yet. When it is, money-movement routes (payouts,
international payments, USDC, multi-currency accounts, cards, cardholders,
physical cards and agents) need the workspace's business verification to be
approved; otherwise they answer 403 with
{"error": "kyb_required", "message": "...", "kybStatus": "..."}. Sandbox
workspaces don't need it. Payouts, deposits and the USDC ramp work only in
sandbox today.
Webhook signing (inbound)
Every webhook we receive from a partner is checked before we act on it (a signature, or a shared secret for partners that don't sign), and repeats of the same event are ignored.
Webhook signing (outbound)
Each endpoint has its own signing secret. Every delivery carries
sorenpay-signature: t=<unix>,v1=<hmac>, where v1 is the hex HMAC-SHA256 of
<t>.<raw body>. See Notifications.