Skip to main content
ER ExaRoutes

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

StatusMeaningCommon 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.

SurfaceLimitWindow
Login5 / IP15 min
Register3 / IP1 hour
Password reset3 / IP1 hour
MFA verify10 / IP15 min
Step-up auth5 / IP15 min
Public free QR generator (POST /api/qr/public/generate)10 / IP24 hours
Public form submissions30 / IP1 min
Serial verify (POST /api/verify/serial, GET /v/:itemId/:signature)60 / IP1 min
GS1 resolver (/01/<gtin>/…)60 / IP1 min
QR redirect handler (GET /r/:shortId)600 / IP1 min (4xx not counted)
Hosted-page views (landing pages, menus, forms)120 / IP1 min
Bulk operations (workspace-bulk-generate, workspace-bulk-replace)5 / IP1 min
AI generation (/api/ai/*)30 / IP1 hour
Data export (GDPR)3 / IP1 hour
Health probe (/api/health/ready)120 / IP1 min
Dodo webhook receiver (/api/webhook/dodo)60 / IP1 min
Admin reads60 / IP1 min
Admin destructive20 / IP1 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.