Authentication

Two kinds of credential, what each one opens, and why only one of them may reach a browser.

Every call to the API carries a credential, in the Authorization header.

Authorization: Bearer <credential>

There are two kinds of credential. Confusing them is the most expensive mistake you can make with this API.

First, choose the address. The API exists once per environment, and a credential issued for one is refused by the other. The examples on this page use Production.

EnvironmentAPI base
Productionhttps://api.saphere.ai
Testhttps://api.test.saphere.ai

The two environments are completely 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 things about it are worth stating plainly.

  • It has no limits. It opens every route of the API: creating measurements, reading them back, deleting them, issuing tokens.
  • It has no expiry. Nothing about the key itself limits how long it is valid.
  • The only thing that stops it is your account being deactivated. Revoking a key means deactivating the account, which cuts off every integration using it at the same moment.

The key must never reach a browser or a mobile bundle

A key shipped inside client code is a key anyone can read: by opening the browser’s developer tools, by unpacking a mobile app, or by fetching your files from a cache. And since revoking it means deactivating the account, a leak is not a small incident.

The widget is built so that it never needs the key. It asks your server for a short-lived token instead. Use that route.

Access tokens

An access token is the credential you can hand to a browser. Your server creates it from your API key, with POST /access-tokens. It is deliberately weak.

API keyAccess token
ScopeEverythingOnly the permissions it names
ExpiryNoneMandatory, at most 48 hours
ReplayableAlwaysOnly if you allow it
RevocationDeactivate the accountDELETE /access-tokens/{id}
Safe in a browserNoYes, by design

Two permissions exist: measurement.scan, the Saphere Scan measurement, and measurement.realtime, the real-time chain. They allow opening a measurement session, and nothing else. A token that carries one cannot read your measurements, cannot delete anything, and cannot create other tokens.

Give it maxSessions: 1 and it opens exactly one session. That is what a widget integration should use: a token spent by the measurement it was created for, and worth nothing 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 only once, and it is what the widget presents. id names the token: it is what you pass to revoke it, and using it as a credential opens nothing.

Only a fingerprint of value is stored, so a copy of the database hands out no usable token.

Rate limiting

Requests are counted per credential and per route, over a rolling minute. Each route holds a count of its own, so a burst on one of them never closes another.

RouteLimit
POST /access-tokens20 per minute
Everything else300 per minute

Creating tokens 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.

These bounds are sized for peaks rather than for steady traffic: they are there so that a burst does not turn into an outage. What governs your volume is your quota, not this table.

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, the last one in seconds. Exceeding a limit returns 429, with a Retry-After header in seconds: the route stays closed to that credential for up to a minute.

Opening a measurement

The handshake that opens a Saphere Scan measurement — whether it presents an access token or a measure id — is counted separately, per client rather than per route:

What is countedLimit
measurements opened2 per second
measurements opened20 per minute

The two windows go together: the second spreads out what the minute allows, a measurement holding memory for as long as it runs. An opening refused by this bound does not consume the access token it presented: the token stays usable, and a fresh attempt a moment later goes through.

These values leave a wide margin on what a user journey produces — a person does not start two measurements within the same second. You meet them when automating: a test harness that chains measurements has to space them out.

A useful diagnostic

If a response carries X-RateLimit-* headers, the request reached a route and was refused by a check. If a 404 carries none — and its body is the plain text Not Found — no route matched at all: the path is wrong, rather than the resource.

Error responses

Errors are JSON, with one exception worth knowing about.

{
  "statusCode": 400,
  "message": ["duration must be an interval amounting to more than zero, such as 2h 30min, 45min or 90s, each unit appearing at most once, and to at most 48h"],
  "error": "Bad Request"
}

A path that matches no route answers in plain text: a 404 whose body is Not Found, with no JSON. Every other error, a 404 for a resource that does not exist included, is JSON — { "message": "Not Found", "statusCode": 404 }. If your code parses error bodies as JSON, check the Content-Type first, or a mistyped path will fail on the parsing instead of being handled by its status.

StatusMeaning
400The request body or query failed validation; message lists what.
401Authorization header missing or malformed, unknown API key, or deactivated account.
404No such resource, or not yours. Plain text only when no route matched the path.
406Your Accept header rules out JSON on a route that answers JSON. Send application/json or */*.
415A route taking a JSON body received another Content-Type.
429Rate limit exceeded; Retry-After says when to come back.
500The request could not be completed on our side.
503The 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 account returns exactly the same 404 as one that never existed. This is deliberate: nobody can use the API to find out whether an identifier is real.