Docs · Concepts
Authentication
Every merchant endpoint is authenticated with a bearer API key that carries its own environment, scopes and expiry.
Bearer tokens
Every /v1/* route except the public status poll requires an
Authorization header:
Authorization: Bearer vk_test_... (or vk_live_...)A missing, malformed, revoked or unknown key all return the same
401 invalid_api_key. The message never distinguishes "no such key" from
"revoked key" from "wrong tenant" — the API is not an oracle for which keys exist.
There is a second credential kind. A device token
(vd_…) is what a venue till or tablet carries instead of a key; it goes
in the same header, resolves through the same hook, and differs only where a device
is deliberately narrower — see Devices API.
How keys are stored
The prefix carries the environment: vk_test_ or
vk_live_. Valitel never stores the raw key — only a SHA-256 hash and the
first 12 characters as a display prefix (vk_test_3fk2…), which is what
the dashboard lists. A leaked database dump therefore contains no usable
credentials, and a key that was not copied at mint time cannot be recovered.
Scopes
Every key carries a set of scopes, chosen when it is minted. There are four:
| Scope | Grants |
|---|---|
verifications:create | POST /v1/verifications |
verifications:read | GET /v1/verifications, GET /v1/verifications/:id, POST /v1/verifications/:id/redeem |
verifications:delete | DELETE /v1/verifications/:id |
devices:manage | POST /v1/devices, GET /v1/devices, DELETE /v1/devices/:id |
Redeem counts as a read: it fetches a result that already exists. Erasure is its own scope because reading a claim and destroying the record of it are different powers, and only one of them is irreversible — the mint form offers it unticked, so a key created by accepting the defaults cannot erase anything.
A key presented to a route it lacks the scope for gets
403 insufficient_scope, and the message names the missing scope. The
check runs before the request body is parsed, so a malformed payload on a route you
are not scoped for is still a 403, never a 400.
Authentication is decided first: an unknown or expired key always gets its
401.
Expiry
A key may be minted with a deadline — the dashboard offers never, 30, 90 or
365 days, and never is the default. Past it the key answers
401 api_key_expired, a distinct code from invalid_api_key
so an integration can tell "rotate me" from "this was never valid". The boundary is
inclusive: a key expiring at exactly now is already expired.
The routes that need no key
Three routes are deliberately unauthenticated:
GET /v1/verifications/:id/status— the poll the widget makes from the merchant's own page. It returns{id, status}and never claims.GET /wallet/request/:sessionIdandPOST /wallet/response/:sessionId— the wallet's two calls. The unguessable session id in the path is the capability.