Authentication and errors
Authenticate with an API-scoped key via X-Api-Key or Bearer, understand the two scopes (API versus Ingest), tell 401 from 403, read the full error code table with troubleshooting, and handle 429 rate limiting with a Retry-After backoff example.
How authentication works
Every endpoint except GET /v1/health requires an API key. Send it one of two ways — pick whichever suits your client:
# Header form 1
X-Api-Key: ta_live_xxxxxxxxxxxxxxxxxxxxxxxx
# Header form 2 (equivalent)
Authorization: Bearer ta_live_xxxxxxxxxxxxxxxxxxxxxxxx
Both are checked on every request; you never need to send both. There is no login step, no token exchange, and no session — the key is the credential.
The two scopes
An API key carries one or more scopes, set when it is created under Settings -> API Keys (an administrator action):
| Scope | Grants | Public API access? |
|---|---|---|
| API | Access to the Public API described in these docs. | Yes |
| Ingest | Pushing telemetry through agents and collectors. | No |
To call the Public API, the key must have the API scope enabled. The Ingest scope alone is not enough.
401 versus 403
These two are the most common stumbling blocks, and they mean different things:
| Status | Code | Cause | Fix |
|---|---|---|---|
| 401 | unauthorized |
No key was sent, or the key is wrong, revoked, or expired. | Check the header name and value. Confirm the key still exists in Settings -> API Keys. |
| 403 | forbidden |
The key is genuine but does not carry the API scope. | Edit the key in Settings -> API Keys and enable the API scope, or create a new key with it. |
Note: In short: 401 is about the key's identity (we do not recognize it) and 403 is about the key's permission (we recognize it, but it may not do this).
The full error table
Every non-2xx response has the same shape:
{"error": {"code": "<string>", "message": "<human readable>"}}
| Code | Status | What it means | What to do |
|---|---|---|---|
bad_request |
400 | A field is missing, the wrong type, or out of range (for example a step without an agg). |
Read the message; fix the request. Do not retry unchanged. |
unauthorized |
401 | Missing or invalid API key. | Fix the key or header. Do not retry unchanged. |
forbidden |
403 | Valid key, but it lacks the API scope. | Enable the API scope on the key. Do not retry unchanged. |
not_found |
404 | The requested resource does not exist for your organization. | Check the id or path. Do not retry unchanged. |
rate_limited |
429 | Per-minute rate limit exceeded. | Wait Retry-After seconds, then retry. |
internal_error |
500 | An unexpected error on our side. | Retry with exponential backoff. |
unavailable |
503 | The service is temporarily unavailable. | Retry with exponential backoff. |
Rate limiting and 429 in depth
Each key has its own per-minute budget. When you exceed it, you receive:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{"error": {"code": "rate_limited", "message": "rate limit exceeded"}}
The Retry-After value is a whole number of seconds. Honor it: sleep for at least that long before retrying. A robust client combines Retry-After with exponential backoff and a cap on attempts:
# Bash: retry a POST on 429 and 5xx with backoff, up to 5 attempts
attempt=0
while :; do
attempt=$((attempt+1))
resp=$(curl -s -o body.json -w "%{http_code}" \
-X POST https://api.verops.io/v1/custom/metrics \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d @payload.json)
case "$resp" in
2*) echo "ok"; break ;;
429|5*)
[ "$attempt" -ge 5 ] && { echo "giving up"; break; }
sleep $((2 ** attempt)); ;; # 2s, 4s, 8s, 16s ...
*) echo "client error $resp"; cat body.json; break ;; # 4xx: do not retry
esac
done
To avoid hitting the limit in the first place: batch your writes (up to 1000 items per call — see Send custom data), poll read endpoints on a sensible interval rather than in a tight loop, and spread bursts out over the minute.
Best practice: Retry only429and5xx, and always with backoff. Retrying400,401, or403just repeats a request that cannot succeed until you change something — it wastes your rate budget and delays the real fix.