Authentication
Firmly accepts two distinct authentication patterns. Pick the one that matches where your code runs.
| Pattern | Where your code runs | What you send | Endpoint reference |
|---|---|---|---|
| Browser session | Client-side (browser, mobile app, embedded widget) | App ID → bootstrap → use returned JWT | Browser Session |
| Server-to-server | Backend (your servers, agent backend, integration tier) | Long-lived S2S secret + device identifier | Server-to-Server |
Your App ID is the durable destination credential Firmly issues you. Depending on where your code runs, it either bootstraps a short-lived JWT (browser-session pattern) or is paired with an S2S secret and device identifier (server-to-server pattern). Both result in a per-request x-firmly-authorization header — only the value differs.
The two patterns
Browser session (most common)
The browser-session pattern is what client-side applications use: one bootstrap call returns a JWT, which you then send in x-firmly-authorization on every subsequent request. The Sandbox Setup walks the actual request and response.
Token lifetime is ~1 hour (expires_in: 3600). To renew, re-post to the same endpoint with the prior (expired) token in the body — this returns a new token with the same device_id as long as the session state is still retained (~7 days from last activity). There’s no separate server-enforced renewal window.
# Bootstrapcurl -X POST https://api.firmly.work/api/v1/browser-session \-H "x-firmly-app-id: YOUR_APP_ID"
The bootstrap returns the token and session identity:
{ "access_token": "eyJhbGc...", "device_id": "dev_01H2X...", "device_created": true, "expires_in": 3600 }
Send access_token as x-firmly-authorization on every subsequent call:
curl -X GET https://api.firmly.work/api/v2/domains/staging.luma.gift/cart \-H "x-firmly-authorization: <access_token from above>"
See Browser Session reference for the full field list, renewal pattern, and error cases.
Server-to-server
The S2S pattern is for backend systems that act on behalf of devices/users. The flow:
Firmly issues the S2S secret to you alongside your App ID. There’s no bootstrap call — you put that long-lived secret in x-firmly-authorization on every request, plus a device_id you assign per user/session so Firmly can isolate carts and orders correctly.
curl -X POST https://api.firmly.work/api/v1/discovery/search \-H "x-firmly-authorization: YOUR_S2S_SECRET" \-H "x-firmly-device-id: user-12345" \-H "Content-Type: application/json" \-d '{"query": "shoes"}'
The x-firmly-device-id value is your own user/session identifier — typically your own user ID or a session UUID. It must be ≤ 256 chars and contain only a-z, A-Z, 0-9, -, _. See Server-to-Server reference for the full spec.
Which to pick
| Question | Browser Session | S2S |
|---|---|---|
| Does your code run client-side? | ✅ | ❌ |
| Can the credential safely appear in client code? | App ID (intentional) | No — the S2S secret must stay server-side |
| Do you want to manage device identity yourself? | No (Firmly assigns) | Yes (you assign device_id) |
| Is your code a backend service acting for many users? | Not the fit | ✅ Use S2S |
| Is your code an embedded widget or browser app? | ✅ Use Browser Session | Not the fit |
Most agentic deployments use browser session because the agent is acting client-side or as a per-user session. Backend integrations and batch jobs use S2S.
What’s in the JWT
The POST /browser-session call returns a JSON body with these bootstrap response fields:
| Field | Meaning |
|---|---|
access_token |
The JWT itself |
device_id |
Unique session identifier — store it so you can renew the token later |
device_created |
Boolean — was this device new in this call? |
expires_in |
Seconds until expiry (typically 3600) |
expires |
Unix timestamp of expiry |
The access_token is a standard JWT — not encrypted, just signed. Decode its payload to see the claims inside:
appid— your App IDdeviceid— same as thedevice_idabovesub— partner / destination name bound to your App IDenv— environment (uat,prod, etc.)iss—identity(the issuer)iat/exp— issued at / expires at (Unix timestamps)
Idempotency
Pass an Idempotency-Key header — a UUID your code generates — on mutation requests, and reuse the same key when retrying the same attempt.
curl -X POST .../complete-order \-H "x-firmly-authorization: $TOKEN" \-H "Idempotency-Key: $(uuidgen)" \-d '{...}'
Common auth errors
All errors use the standard REST envelope: { "code": <status>, "error": "<ErrorName>", "description": "..." }.
| Status | Error | Fix |
|---|---|---|
| 400 | MissingAuthHeader |
Add the x-firmly-authorization header |
| 400 | InvalidToken |
Token is malformed (not a valid JWT structure) — re-bootstrap |
| 401 | InvalidJWTToken |
JWT signature failed verification, or the token is expired — bootstrap a new session or renew |
See Errors & Conventions for the full catalog.
Endpoint references
- Browser Session — request/response, renewal, all fields
- Server-to-Server — headers, device-id format, full sample requests