Authentication
Create an API key, trade it for a short-lived access token, and keep that token fresh.
Two credentials, one flow
Programmatic access uses an API key that you create once and keep on your server. The key never reaches the database. You trade it at the token endpoint for a short-lived access token, and that token is what every later request carries. The token is an ordinary session for your own user, so the same row-level rules that scope the dashboard scope your requests. Nothing you reach this way is wider than what your account sees in the UI.
The exchange
- 1Create a key under Dashboard, API keys. It looks like sk_live_ followed by 43 characters and is shown in full exactly once, because only a hash of it is stored. If you lose it, revoke it and create another.
- 2POST the key to the token endpoint as an Authorization Bearer header. There is no request body.
- 3Keep access_token, and note expires_in. The response also carries project_url and publishable_key so an integration can configure itself from this one call.
- 4Send both the publishable key and the access token on every Data API request. Exchange again when the token is close to expiring.
Exchange a key
curl -X POST https://www.saficonfirm.com/api/v1/token \
-H "Authorization: Bearer sk_live_4kQ9xZ2mNp7vR1sT8wY3bC6dF0gH5jK2lM9nP4qR7tV"What comes back
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600,
"project_url": "https://nvkkanirwnjjvuwxsjch.supabase.co",
"publishable_key": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Lifetimes
Two separate clocks: the key you store, and the token it mints.
| Credential | Lives for | Notes |
|---|---|---|
| API key | Until you revoke it | Unless you set expiresInDays between 1 and 365 when you create it, in which case it stops working on its own at that point. |
| Access token | 3600 seconds, one hour | Read expires_in rather than hard coding the hour. Renewing a minute early avoids a token expiring mid request. |
| Refresh token | Not issued | Deliberate. A refresh token would let one exchange renew forever, which is the long-lived credential this design avoids. Re-exchange the key instead. |
Why the token expires
The short life is the security property, not an inconvenience. A token is verified by its signature and its expiry alone, with no lookup that could be told the key behind it was revoked, so a leaked token cannot be recalled. Capping it at an hour bounds that window. Revocation works on the next exchange, which is re-checked against the database every single time, so a revoked key stops minting tokens immediately while a token already in flight lives out its remaining minutes. If a key leaks and you cannot wait out the hour, contact us and the underlying session can be terminated server side.
Caching the token
const API_KEY = process.env.SAFICONFIRM_API_KEY!
const TOKEN_URL = "https://www.saficonfirm.com/api/v1/token"
let cached: { token: string; expiresAt: number } | null = null
export async function getAccessToken(): Promise<string> {
// Renew a minute early so a token cannot expire mid request.
if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}` },
})
if (response.status === 401) {
throw new Error("SafiConfirm API key is invalid, expired, or revoked.")
}
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? 60)
throw new Error(`Rate limited. Retry in ${retryAfter}s.`)
}
if (!response.ok) throw new Error(`Token exchange failed with ${response.status}.`)
const { access_token, expires_in } = await response.json()
cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 }
return cached.token
}Exchange once and reuse the token for its full hour. Exchanging per request will hit the rate limit.
Connection details
SAFICONFIRM_TOKEN_URL=https://www.saficonfirm.com/api/v1/token
SUPABASE_URL=https://nvkkanirwnjjvuwxsjch.supabase.co
SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im52a2thbmlyd25qanZ1d3hzamNoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3NjI0MzQsImV4cCI6MjA3NTMzODQzNH0.Dl_TPxW6K1AhQAoxVNrIZykrxCeoJxck-DTrXuzcFEgThe project URL and the publishable key are public. The publishable key already ships in this page's JavaScript, and every exchange returns both, so they are printed here for anyone who has mislaid them without needing to open the playground. On their own they grant nothing: the database refuses every row until an access token says who you are. The one value that is secret is your API key, and after that the service role key, which stays on our servers and is never handed out.
Keep the key on your server
An API key is a long-lived credential for your whole account. Put it in your server environment, where you keep a database password, and never ship it to a browser, a mobile app, or anything else a user can read. The token endpoint sends no CORS headers, so a page on another origin cannot call it from a browser even if the key ended up there. Our own playground can, because it is served from this domain and the request is same origin. That is a tool you drive by hand with a key you chose to paste, not a pattern to copy into an application.
Exchange failures
The 401 is deliberately identical for every reason a key can fail. Telling you which keys were merely revoked would also tell someone working through a stolen list which ones are worth pursuing. Check the dashboard for the real state of your keys.
| Status | Body | Cause |
|---|---|---|
| 401 | { "error": "invalid_api_key" } | The key is missing, malformed, unknown, expired, or revoked. |
| 429 | { "error": "rate_limited" } | Too many exchanges. Honour the Retry-After header. 60 per IP and 12 per key, each per 5 minutes. |
| 500 | { "error": "server_error" } | Our side failed to mint the session. Retry with backoff. |
The session cookie path
The routes under https://www.saficonfirm.com/api, such as /api/orders and /api/credits, are the dashboard's own and authenticate with the Supabase session cookie the browser holds. They do not accept an access token, so they are not the server to server path. Everything you do with a key goes to the Data API instead, which is described in the next section. Key management itself, listing, creating, and revoking keys, also runs on the cookie, because credentials should not be able to mint more of themselves.