Skip to main content
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:
  1. Clock drift. X-Timestamp must be within 5 minutes of Spritz server time, in milliseconds. A drifting container clock is the most common cause.
  2. 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.
  3. Query canonicalization. Params sorted by key, URL-encoded, no duplicate keys — duplicates are a 401, not a last-wins.
  4. The user bearer. User-scoped endpoints need all four headers; integrator endpoints are signed with the three signing headers only — no Authorization.