The Spritz API is primarily a backend-to-backend integration. In most cases your
servers talk directly to Spritz, acting on behalf of your users,
and you authenticate those requests by signing them with HMAC.
There are also times when you want a user’s own device (their browser or mobile app)
to call Spritz directly. For those cases you mint a short-lived token on your backend
and hand it to the device, which then uses it to authenticate its own requests.
Two integration shapes
Both are fully supported, and neither is “the advanced one” — pick per use case:
- Server-to-server: your backend talks to Spritz with HMAC, stores what it needs,
and serves your clients however you like. Webhooks keep your copy in sync. Choose this
when you want to own the data model, decorate Spritz data with your own, or present one
unified API to your clients. The trade-off: you’re maintaining a mirror, and mirrors
drift — webhooks plus periodic reconciliation against the API keep you honest.
- Client-direct tokens: your backend does the minimum — create Spritz users and keep
the reference — and mints a short-lived token when a client needs data. Choose this
when you don’t want to be in the data business. The trade-off: clients handle token
refresh, and you see less of what users are doing without querying the API.
They aren’t mutually exclusive. Many integrators run both: server-to-server for the
money-moving and compliance-sensitive paths (creating users, adding bank accounts,
initiating payouts), client-direct tokens for read-heavy screens (transaction history,
status polling). Client-direct is not a lesser path — the Spritz app itself uses the
public API exactly this way, so it’s a well-trodden, well-optimized route.
Acting on behalf of your users
Almost everything in the Spritz API happens on behalf of one of your users. When you
create an off-ramp, connect a bank account, or fund a card, you’re doing it for a
specific user, and your credentials tell Spritz which user that is.
The exception is the integrator endpoints. Those are about you, not your users.
They cover things like your integrator profile, your API keys, and your webhook
configuration. A simple way to hold the distinction: user endpoints act on your
users’ resources, and integrator endpoints manage your own integrator account.
Because integrator endpoints aren’t tied to a user, you don’t send a user API key
with them. You sign them with your integrator credentials only (the three signing
headers below, without the Authorization header).
For a backend request to a user-scoped endpoint, HMAC and the user bearer are one
combined authentication mode: all four headers are required. A short-lived spr_
integrator token is a separate frontend authentication mode and does not use HMAC.
Backend to backend with HMAC
This is the primary way to use the Spritz API. Your backend signs each request with
HMAC-SHA256, so Spritz can confirm it really came from you and that nothing was
changed in transit.
Signing produces three headers. Alongside them you send Authorization to identify
the user you’re acting for (this one isn’t part of the signature).
How the signature is built
You sign a canonical string that pins together the timestamp, method, path, and a hash
of the body:
timestamp is the same value you send in X-Timestamp
METHOD is the HTTP method in uppercase (GET, POST, and so on)
path is the request path including its query string, with query params sorted by
key and URL-encoded (for example /v1/off-ramps?limit=10&status=pending)
bodyHash is the SHA-256 hex digest of the raw request body, or an empty string when
there’s no body
Duplicate query keys are not allowed on HMAC-signed requests. The API rejects them
with 401; use one value per key.
Example
Here’s the full signing algorithm. It uses the Web Crypto API, so it runs as-is on
Node 18+, Bun, Deno, Cloudflare Workers, and browsers.
Then send the signed headers along with the Authorization header for the user:
Your timestamp has to be within 5 minutes of Spritz’s server time, otherwise the
request is rejected. This keeps a captured request from being replayed later.
From a user’s device with an integrator token
Sometimes you’d rather have the user’s device call Spritz directly instead of routing
everything through your backend. For that, your backend mints a short-lived integrator
token (it starts with spr_) and passes it to the device. The device then sends it in
the Authorization header — no signing headers, no user API key:
Because the token is short-lived and minted per user, you can safely hand it to a
browser or mobile app without exposing your backend credentials. It works on every
user-scoped endpoint — including quote creation and transaction parameters — but prefer
server-to-server for money-moving paths: a token expiring mid-flow is a recovery you
don’t have to have.
Minting a token
Exchange HMAC-signed integrator credentials (no user bearer — this is an integrator
endpoint) for a token scoped to one user:
expiresIn is optional, in seconds, default and maximum 3600 (one hour). An unknown
userApiKey — or one for a user that isn’t yours — gets a 400, not a 401. Minting
is rate-limited to 100 requests per minute per integrator, so mint per session, not per
request.
When something’s wrong
A missing or invalid credential returns a 401. A valid credential that isn’t allowed
to touch a particular resource returns a 403. Both come back as
problem responses you can parse the same way as any other error.
Every credential failure — bad signature, unknown key, expired or stale timestamp,
tampered body — returns the same generic 401 with detail: "Authentication failed".
That’s deliberate: the response never reveals which part failed. When you hit a mystery
401, check in this order:
- Clock drift.
X-Timestamp must be within 5 minutes of Spritz server time, in
milliseconds. A drifting container clock is the most common cause.
- Body serialization. Sign the exact bytes you send. If your JSON serializer
reorders keys or adds whitespace between signing and sending, the body hash won’t
match.
- Query canonicalization. Params sorted by key, URL-encoded, no duplicate keys —
duplicates are a
401, not a last-wins.
- The user bearer. User-scoped endpoints need all four headers; integrator
endpoints are signed with the three signing headers only — no
Authorization.