Authentication

Every request to /api/v1 must carry a valid API key. There are two ways to supply it.

On this page

Request header (preferred)

X-Api-Key: ev_your_api_key_here

This is the recommended approach. The key is never logged by proxies that only record the request URL.

Query parameter (alternative)

GET /api/v1/credits?api_key=ev_your_api_key_here

Useful for quick manual testing in a browser or curl one-liner, and the only option for a browser EventSource, which cannot set custom headers. Avoid it in production — many logging systems record query strings, which would expose your key in plaintext.

Key storage

Keys are stored as SHA-256 hashes. We cannot show you a key again after it is created, so copy it when it is revealed.

The key list shows the last 4 characters of the key itself, so you can match a row to the value in your configuration. Keys created before this was recorded show ···· instead: only the hash was ever stored, and the real last 4 cannot be recovered from a hash. That is why older keys look different from new ones — nothing is wrong with them.

Scopes

A key can be narrowed to what it is allowed to do:

Scope Grants
read Every GET on the public surface, including the list endpoints and the event stream
verify The credit-spending submissions — POST /api/v1/verify, POST /api/v1/verify/batch — and POST /api/v1/webhooks/test, which causes an outbound delivery

Choose scopes when you create the key. Choosing none means all scopes, so a key created without thinking about it behaves exactly like a key created before scopes existed.

A key that lacks the scope a route requires gets 403 with code: "insufficient_scope":

{
  "status": 403,
  "code": "insufficient_scope",
  "error": "This API key is not scoped for this endpoint. It requires the 'verify' scope."
}

This is a 403 and not a 401 on purpose: the credential is fine, so retrying with the same key can never help — you need a differently scoped key.

Keys with no scopes recorded keep full access. Every key created before scopes existed stores nothing, and those keys authenticate everywhere exactly as they did before. Existing integrations do not need to be touched.

POST /api/v1/webhooks/test requires verify, not read. It spends no credits, but it lands an HTTP request on an endpoint you nominate, and a key described to its holder as read-only must not be able to cause an outbound side effect.

Expiry

A key can be given an expiry date when it is created. Past that instant it returns 401 with code: "invalid_api_key" — the same answer as an unknown or revoked key, because from a caller's point of view they are the same failure.

Rotating a key without downtime

Revocation used to be instantaneous, which made replacing a key a race: create the new one, deploy, revoke the old one, and hope nothing was in flight between steps two and three.

Rotate removes the race. It mints a successor immediately and schedules the predecessor's revocation 24 hours later. Both keys authenticate for the whole window, so you can roll a deploy at your own pace; the old key then dies on its own and starts returning 401.

The key list shows the relationship — which key replaced which, and when the predecessor stops working — so a half-finished rollout is visible rather than remembered.

Rotating a different key while another key's overlap window is still open is fine; each key carries its own deadline and keeps every hour it was promised. Rotating the same predecessor a second time is refused, because that would move a deadline its successor was already promised.

Propagation

Resolved keys are cached for 30 seconds, but the two deadlines you actually schedule are exact, not eventually-consistent:

  • Expiry (expiresAt) and scheduled revocation (the retirement a rotation sets on the old key) take effect immediately, to the instant. Both are known future times, so a cached entry is re-checked against the clock on every hit and dropped the moment one passes — the request then falls through to the same check against the database and is refused. The cache cannot extend either deadline.
  • An immediate manual revoke is the one operation the 30-second window applies to, and only when it happens in a different process from the one serving the request. A revoke handled by the process serving you evicts its own cache entry at once; a revoke handled elsewhere is honoured within 30 seconds. Plan a cutover with that in mind — it is bounded.

Key management is dashboard-only

Keys are created, scoped, rotated and revoked in Dashboard → API. There is no public API for key management, and there will not be one.

The reason is straightforward: a key that can mint keys is a privilege-escalation path. Anyone who copied that key out of a log or a CI variable could issue themselves a fresh, unexpired, fully-scoped credential and survive the revocation of the one they stole. Payment APIs do not expose secret-key creation either, for the same reason. Key lifecycle stays behind an authenticated human session.

Error responses

Missing key:

{
  "status": 401,
  "code": "unauthorized",
  "error": "API key required."
}

Unknown, revoked, expired, or past its scheduled revocation:

{
  "status": 401,
  "code": "invalid_api_key",
  "error": "Invalid or revoked API key."
}

The two are distinguishable by code, never by the message text. See Errors.

Test-mode keys

Keys that begin with ev_test_ are test-mode keys. They return instant canned results, cost zero credits, and are safe to use in CI pipelines. See Test mode.

A key's ev_test_ prefix and its test-mode flag are enforced to agree at creation, so the prefix is a reliable signal rather than a label: if it starts with ev_test_, it spends nothing.

A key sees only its own mode's resources

Test and live are separate namespaces, not two labels on one pile of data. A key can read only the verifications and batch jobs that were created in its own mode:

  • GET /api/v1/verify and GET /api/v1/verify/batch list only that mode's resources.
  • GET /api/v1/verify/{id} and GET /api/v1/verify/batch/{id} answer 404 not_found for a resource belonging to the other mode — the same answer another account's id gets. No download URL is issued, and nothing in the response confirms the id exists.
  • Idempotency-Key values are likewise per-mode: the same key used in both modes is two unrelated requests, never a replay of the other.

So a sandbox key in CI cannot reach — or download — anything real, and a live listing never has canned sandbox verdicts mixed into it.

Every verification and batch job carries livemode: true for live, false for sandbox. Because reads are mode-scoped it is constant for any one key, which is the point — a stored response, a log line or a webhook body says for itself which mode produced it.