> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spritz.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How your platform authenticates with the Spritz API.

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](/guides/definitions#user),
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.

## 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).

## 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).

| Header             | Value                                                        |
| ------------------ | ------------------------------------------------------------ |
| `X-Integrator-Key` | Your integrator API key (`int_…`)                            |
| `X-Timestamp`      | Unix timestamp in milliseconds                               |
| `X-Signature`      | `sha256=<hex>`, computed as below                            |
| `Authorization`    | `Bearer <user-api-key>`, the user you're acting on behalf of |

### How the signature is built

You sign a canonical string that pins together the timestamp, method, path, and a hash
of the body:

```
{timestamp}.{METHOD}.{path}.{bodyHash}
```

* `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

### 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.

```ts theme={null}
const SIGNATURE_PREFIX = "sha256="

function hexEncode(buffer: ArrayBuffer): string {
  return Array.from(new Uint8Array(buffer))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("")
}

async function sha256Hex(data: string): Promise<string> {
  const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data))
  return hexEncode(hash)
}

async function hmacSha256Hex(secret: string, data: string): Promise<string> {
  const enc = new TextEncoder()
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  )
  return hexEncode(await crypto.subtle.sign("HMAC", key, enc.encode(data)))
}

// Query params are sorted by key and URL-encoded before signing.
function buildPathWithQuery(url: URL): string {
  const params = url.searchParams
  if ([...params].length === 0) return url.pathname
  const query = [...params.keys()]
    .sort()
    .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params.get(k) ?? "")}`)
    .join("&")
  return `${url.pathname}?${query}`
}

export async function stampRequest(
  integratorKey: string,
  integratorSecret: string,
  method: string,
  url: string,
  body?: string | null,
) {
  const path = buildPathWithQuery(new URL(url))
  const timestamp = Date.now()
  const bodyHash = body ? await sha256Hex(body) : ""
  const payload = `${timestamp}.${method.toUpperCase()}.${path}.${bodyHash}`
  const signature = `${SIGNATURE_PREFIX}${await hmacSha256Hex(integratorSecret, payload)}`

  return {
    "X-Integrator-Key": integratorKey,
    "X-Signature": signature,
    "X-Timestamp": String(timestamp),
  }
}
```

Then send the signed headers along with the `Authorization` header for the user:

```ts theme={null}
const url = "https://platform.spritz.finance/v1/off-ramps"
const body = JSON.stringify({
  /* request fields */
})

const signed = await stampRequest(
  process.env.SPRITZ_INTEGRATOR_KEY!,
  process.env.SPRITZ_INTEGRATOR_SECRET!,
  "POST",
  url,
  body,
)

const res = await fetch(url, {
  method: "POST",
  headers: {
    ...signed,
    "Content-Type": "application/json",
    Authorization: `Bearer ${userApiKey}`,
  },
  body,
})
```

<Warning>
  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.
</Warning>

## 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:

```bash theme={null}
curl https://platform.spritz.finance/v1/users/me \
  -H "Authorization: Bearer spr_…"
```

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.

## 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](/guides/errors) you can parse the same way as any other error.
