truval.dev APIs return JSON error bodies with a consistent shape. Use the error field for branching in code; show message and action to humans.
Most errors use this structure (from the verify and management workers):
| Field | Type | Purpose |
|---|
error | string | Stable machine code (this page is the catalog). |
message | string | Short human-readable explanation. |
action | string | What the client or operator should do next. |
docs | string | Deep link into these docs or OpenAPI. |
Optional fields depend on the error (rate limits, quota, spend cap). 402 payment_required on verify routes uses hard_cap_eur and current_overage_eur instead of always matching the full envelope (still includes error, message, action, docs).
OpenAPI: https://api.truval.dev/openapi.json documents many verify errors; management and some edge cases are listed here as the full catalog.
The developer dashboard Concrete logs table shows error_class from usage_log. For most verify failures it matches the JSON error field. Exceptions:
usage_log.error_class | HTTP response error | Notes |
|---|
rate_limit | rate_limit_exceeded | Same event: per-minute cap. Always use error in API clients. |
(varies, e.g. TypeError) | (no synchronous body) | Async webhook verification failed in the background. The row may show an exception name as error_class; error_snapshot typically includes "error": "internal". |
Successful verifications use error_class: null.
The remote MCP server surfaces HTTP-style errors inside JSON-RPC error.data. See MCP Server for the mapping; the codes below are the same error strings.
Subsection titles match the error string for deep links (for example #rate_limit_exceeded).
| |
|---|
| HTTP | 401 |
| APIs | Verify (/v1/email/*), Management (/v1/management/*) |
| Cause | No Authorization header or not Bearer …. |
| Fix | Send Authorization: Bearer sk_live_… for verify, or sk_mgmt_… for management. |
| Extra fields | — |
| |
|---|
| HTTP | 401 |
| APIs | Verify, Management |
| Cause | Key hash not in KV (revoked, typo, wrong environment). |
| Fix | Create a new key in the dashboard or rotate with your provisioning key. |
| Extra fields | — |
| |
|---|
| HTTP | 403 |
| APIs | Verify (if sk_mgmt_… or provisioning key), Management (if not sk_mgmt_… provisioning key) |
| Cause | Key type does not match the route family. |
| Fix | Use sk_live_… on verify routes; use sk_mgmt_… on /v1/management/* (except dashboard-only bootstrap). |
| Extra fields | — |
| |
|---|
| HTTP | 413 |
| APIs | Verify (when Content-Length is present and over limit) |
| Cause | Request body exceeds the advertised limit (header guard; 2MB). |
| Fix | Shrink the body; batch large workloads across requests. |
| Extra fields | — |
| |
|---|
| HTTP | 400 |
| APIs | Verify (JSON shape, email/webhook validation), Management (JSON body rules) |
| Cause | Malformed JSON, missing fields, invalid webhook URL, wrong batch/stream array bounds, etc. |
| Fix | Follow action and docs in the response; see Email verify (request / batch / stream sections). |
| Extra fields | — |
usage_log error_class | Usually invalid_request |
| |
|---|
| HTTP | 503 |
| APIs | Verify |
| Cause | Temporary failure while checking free-tier monthly quota (e.g. database). |
| Fix | Retry with backoff; contact support if persistent. |
| Extra fields | — |
usage_log error_class | quota_check_failed |
| |
|---|
| HTTP | 429 |
| APIs | Verify |
| Cause | Free tier: UTC calendar month verification unit cap would be exceeded. |
| Fix | Wait until after reset_at (start of next UTC month) — add a small cushion for clock skew/latency — or upgrade. Do not tight-loop. |
| Extra fields | limit, used, reset_at |
usage_log error_class | monthly_quota_exceeded |
Free-tier quota: Rows appear in Concrete logs but failed_check monthly_quota_exceeded is excluded from the monthly cap count (see monthly quota).
| |
|---|
| HTTP | 429 |
| APIs | Verify |
| Cause | Per-minute rate limit for the key’s tier exceeded (batch/stream count per email toward the limit). |
| Fix | Wait until after reset_at (add a small cushion for clock skew/latency), throttle clients, or upgrade tier for higher limits. |
| Extra fields | limit, window ("1m"), reset_at |
usage_log error_class | rate_limit (not rate_limit_exceeded) |
Free-tier quota: Rows appear in Concrete logs but failed_check rate_limit_exceeded is excluded from the monthly cap count (see monthly quota).
| |
|---|
| HTTP | 402 |
| APIs | Verify |
| Cause | Hard EUR spend cap for the billing period would be exceeded (preflight uses worst-case email count for the request). |
| Fix | Raise or clear the hard cap under Billing & Limits, or upgrade. |
| Extra fields | hard_cap_eur, current_overage_eur (may be null for cap in edge cases) |
usage_log error_class | payment_required |
| |
|---|
| HTTP | 404 |
| APIs | Verify (unknown route), Management (unknown route; customer not found on provisioning bootstrap) |
| Cause | Wrong path, or (management internal bootstrap) invalid customerId. |
| Fix | Use documented paths and OpenAPI; check customer UUID for provisioning. |
| Extra fields | — |
| |
|---|
| HTTP | 404 |
| APIs | Management (DELETE /v1/management/keys/:id) |
| Cause | No active standard key with that id for the account. |
| Fix | List keys with GET /v1/management/keys and use a valid id. |
| Extra fields | — |
| |
|---|
| HTTP | 429 |
| APIs | Management (POST /v1/management/keys) |
| Cause | Active standard keys already at plan maximum. |
| Fix | Revoke a key, then create a new one, or upgrade tier for higher max_keys. |
| Extra fields | — |
| |
|---|
| HTTP | 402 |
| APIs | Management (POST /v1/management/billing/portal) |
| Cause | No Stripe customer id on file for the account. |
| Fix | Complete billing setup in the dashboard, then retry. |
| Extra fields | — |
| |
|---|
| HTTP | 500 |
| APIs | Management (billing portal session creation) |
| Cause | Stripe API did not return a portal URL. |
| Fix | Retry later; if ongoing, check Stripe status and account configuration. |
| Extra fields | — |
| |
|---|
| HTTP | 500 (most), 409 (provisioning key already exists) |
| APIs | Management |
| Cause | Supabase request failed or conflict (e.g. provisioning key already present). |
| Fix | Retry for transient failures; for 409 on provisioning, revoke the existing provisioning key first per action text. |
| Extra fields | — |