Authentication
Two credentials with very different powers: the API key that identifies your account, and the access tokens it mints.
Every call to the API carries a bearer credential:
Authorization: Bearer <credential>
There are two kinds, and confusing them is the most expensive mistake available.
Before either: pick the host. The API exists once per environment, and a credential issued for one is refused by the other. Examples on this page use Production.
| Environment | API base |
|---|---|
| Production | https://api.saphere.ai |
| Test | https://api.test.saphere.ai |
The two are fully separate — separate accounts, separate keys, separate data. See Environments.
The API key
Your API key identifies your client account. It is a plain UUID.
POST /access-tokens HTTP/1.1
Host: api.saphere.ai
Authorization: Bearer 2428fbbc-d322-49cb-90a7-4e93401e67f8
Content-Type: application/json
Three properties of it are worth stating plainly:
- It has no scope. It opens every route of the API — creating measurements, reading them back, deleting them, minting tokens.
- It has no expiry. Nothing about the key itself limits how long it is valid.
- The only thing that bounds it is your account being active. Revoking a key means deactivating the client, which cuts every integration using it at the same instant.
The key must never reach a browser or a mobile bundle
A key in shipped client code is a key in the hands of anyone who opens developer tools, unpacks an APK, or reads a bundle from your CDN cache. Because revoking it means deactivating the account, a leak is not a small incident.
The widget is designed so that it never needs the key: it asks your server for a short-lived token instead. Use that path.
Access tokens
An access token is the credential you hand to a browser. It is minted from your API key by POST /access-tokens, and it is deliberately weak:
| API key | Access token | |
|---|---|---|
| Scope | Everything | Only the permissions it names |
| Expiry | None | Mandatory, at most 48 hours |
| Replayable | Always | Only if you allow it |
| Revocation | Deactivate the account | DELETE /access-tokens/{id} |
| Safe in a browser | No | Yes, by design |
Today one permission exists, websocket.measurement: it allows opening a real-time measurement session, and nothing else. A token carrying it cannot read your measurements, cannot delete anything, and cannot mint further tokens.
Give it maxAttempts: 1 and it opens exactly one session. That is the shape to use for a widget integration: a token that is spent by the measurement it was created for, and worthless afterwards.
The token value and the token id are different strings
POST /access-tokens returns both. value is the credential — it begins with ivat-, it is returned once, and it is what the widget presents. id is a UUID that names the token: it is what you pass to revoke it, and presenting it as a bearer opens nothing.
Only a hash of value is stored, so a database dump does not hand out usable tokens.
Rate limiting
Requests are counted per credential, over a rolling minute.
| Route | Limit |
|---|---|
POST /access-tokens | 10 per minute |
| Everything else | 120 per minute |
Minting is deliberately tighter than the rest: each call writes to the database, and a token is meant to be requested once at the start of a measurement — never in a loop.
Responses carry the usual x-ratelimit-* headers. Exceeding a limit returns 429.
A useful diagnostic
The presence ofx-ratelimit-* headers on a response tells you the request reached a route and was refused by a guard. Their absence on a 404 tells you no route matched at all — a wrong path, rather than a wrong resource.Error responses
Errors are JSON, with one exception worth knowing about.
{ "statusCode": 400, "message": ["duration must be an interval"], "error": "Bad Request" }
Every 404 is text/plain, with the body Not Found. This holds whether the path matched no route at all or the route refused to find the resource. If you parse error bodies as JSON, special-case 404 or you will throw on the parse instead of handling the status.
| Status | Meaning |
|---|---|
400 | The request body or query failed validation; message lists what. |
401 | Missing or malformed Authorization header. |
403 | The credential does not carry the permission the route requires. |
404 | No such resource, or not yours. Plain text. |
429 | Rate limit exceeded. |
503 | The instance is shedding load; Retry-After says when to come back. |
Not found and not yours are the same answer
A resource belonging to another client returns exactly the same404 as one that never existed. This is deliberate: it means nobody can use the API to discover whether an identifier is real.