Errors
The API uses standard HTTP status codes. The response body for any non-2xx
is JSON with a single error field describing what went wrong.
Body shape
HTTP/1.1 400 Bad Request
Content-Type: application/json
{ "error": "Invalid dataType provided." }
Some hardened endpoints return additional fields (e.g. multer upload limits
return { "error": "...", "code": "LIMIT_FILE_SIZE" }). These
are documented at the endpoint that emits them.
Status codes
| Status | Meaning | Common causes |
|---|---|---|
400 | Bad Request | Missing or invalid field in body / headers. The error message names the field. |
401 | Unauthorized | Invalid or revoked API key, or expired session token. |
403 | Forbidden | Auth valid but the calling user doesn't have access to the requested resource (e.g. cross-workspace target). Also returned when a step-up MFA challenge is required and not provided. |
404 | Not Found | API key not found (typo) or resource id doesn't exist / isn't visible to the caller. |
409 | Conflict | State precondition failed (e.g. trying to verify a domain that's already verified). |
410 | Gone | Returned by the redirect handler when a QR is expired, has reached its scan cap, or was deleted. |
413 | Payload Too Large | Request body exceeded the per-endpoint size limit (25 MB JSON, 10 MB per file in multipart uploads). |
422 | Unprocessable | Validation failed at the field level (e.g. an unsupported QR dataType or an invalid hex color in dotsOptions). |
429 | Too Many Requests | Rate limit hit. The Retry-After header (when present) indicates the cool-down window in seconds. See Rate limits. |
500 | Internal Server Error | Unexpected failure. Includes a Sentry-correlatable id in our logs. If you see this repeatedly, contact support with the timestamp + request shape. |
502 / 503 | Upstream / Unavailable | Backend dependency (e.g. DynamoDB) reported unhealthy. Status page: /status. |
Rate limits
All per-IP limits below use a 429 response. RateLimit-* draft-7 headers are returned on every limited route. Retry-After is included when applicable.
| Surface | Limit | Window |
|---|---|---|
| Login | 5 / IP | 15 min |
| Register | 3 / IP | 1 hour |
| Password reset | 3 / IP | 1 hour |
| MFA verify | 10 / IP | 15 min |
| Step-up auth | 5 / IP | 15 min |
Public free QR generator (POST /api/qr/public/generate) | 10 / IP | 24 hours |
| Public form submissions | 30 / IP | 1 min |
Serial verify (POST /api/verify/serial, GET /v/:itemId/:signature) | 60 / IP | 1 min |
GS1 resolver (/01/<gtin>/…) | 60 / IP | 1 min |
QR redirect handler (GET /r/:shortId) | 600 / IP | 1 min (4xx not counted) |
| Hosted-page views (landing pages, menus, forms) | 120 / IP | 1 min |
Bulk operations (workspace-bulk-generate, workspace-bulk-replace) | 5 / IP | 1 min |
AI generation (/api/ai/*) | 30 / IP | 1 hour |
| Data export (GDPR) | 3 / IP | 1 hour |
Health probe (/api/health/ready) | 120 / IP | 1 min |
Dodo webhook receiver (/api/webhook/dodo) | 60 / IP | 1 min |
| Admin reads | 60 / IP | 1 min |
| Admin destructive | 20 / IP | 1 hour |
API-key endpoints (POST /api/qr/generate,
POST /api/qr/bulk-generate) are not capped per-IP. They're
governed by your plan's QR-creation quota. 429 on plan
saturation. The body explains which quota saturated (e.g.
"Plan limit reached: dynamic_qr_count (5/5).").
All limits are app-layer for v1. A WAF/edge layer is planned for production to add per-IP burst protection on the auth + payment endpoints.
Plan quota responses
When a plan-level limit is hit (QR count, scan count, dynamic-QR seats), the API returns:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "error": "Plan limit reached: dynamic_qr_count (5/5)." } Open Settings → Billing to upgrade, or wait for the monthly window to reset.
Idempotency
POST /api/qr/generate creates a new resource on every successful call.
We do not yet support an Idempotency-Key header. If your client
retries on transient errors (5xx, network), check the dashboard's QR list
or use a 1-row bulk-generate with the
returned bulkId as your dedup key.
Reporting issues
For 5xx responses you can't reproduce, email support@exaroutes.com with the timestamp (UTC), the endpoint, and the request body (redact secrets). Include any error message verbatim. We correlate by message + timestamp.