API docs → Errors
Error reference
Every error returned by the API. error is the stable machine code — switch on it,
never on message (which is for humans and may change).
Response shape
Every error follows the same envelope:
{
"error": "rate_limit_exceeded", // stable machine code
"message": "Monthly limit reached: 5 detections.",
"request_id": "0190b1f8-aae5-7e3a-9b1c-7f4ba2c0d3e1",
"details": { // optional, per-error context
"used": 50,
"limit": 50,
"resets_at": 1717948800000,
"retry_after_seconds": 28800
}
} Always include request_id in support tickets — we use it to pull every log line and
downstream call for that request.
Codes
| HTTP | Code | Cause | Fix |
|---|---|---|---|
400 | invalid_input | Body or query parameter failed Zod validation. Common: text below minLength, strictness outside enum, mc_k out of range. | Inspect `details` — Zod returns the exact field + message. Re-send with the right shape. |
401 | unauthorized | Missing `Authorization` header, malformed token, revoked or unknown key. | Confirm the header is `Authorization: Bearer dad_live_...`. Generate a fresh key in your dashboard if needed. |
402 | upgrade_required | Word count exceeds tier max, or the endpoint (deep / plagiarism) requires a paid tier. | Truncate the input, or upgrade. The error includes `upgrade_url` and `upgrade_features` so you can hand the user a deep link. |
402 | word_limit_exceeded | Per-document word limit hit. Anonymous / free = 1,000 words. Pro/Plus = 5,000. Team = 20,000. | Split into chunks (recommended: paragraph boundaries) or upgrade. |
402 | paid_tier_required | You hit a Pro+ endpoint from a free / anonymous tier. | Upgrade. The response includes a `feature_summary` outlining what unlocks. |
413 | payload_too_large | Request body exceeded 200KB. | Trim the body — 5,000 words is roughly 25-30KB so this only fires on misuse. |
422 | insufficient_text | Input has fewer than 80 words. Below this threshold the prior dominates the score, so we refuse rather than return a misleading number. | Paste more text. `min_words` and `recommended_min_words` are in the response. |
429 | rate_limit_exceeded | Per-tier quota exhausted. Anonymous = 1/calendar-month/IP. Registered free = 5/calendar-month. Paid tiers are unmetered (Pro / Team / Enterprise) or balance-gated (PAYG / API-only). | Wait until `resets_at` (unix ms). The SDKs handle this automatically with backoff. The `options` array in the response gives upgrade paths the user can pick from. |
429 | rate_limited | Alternate code returned on some endpoints. Identical semantics to `rate_limit_exceeded`. | Same as `rate_limit_exceeded`. |
500 | internal_error | Unhandled server-side exception. The response is intentionally opaque (no stack traces leak). | Retry with exponential backoff (max 3 tries). Include the `request_id` in any support ticket. |
502 | deep_scan_failed | ML upstream (Modal) unreachable or returned an error. Only fires on `/v1/detect/deep`. | Retry. Status page (https://deepaidetector.com/status) shows when this is degraded. |
503 | billing_not_configured | Stripe keys not configured (staging / fresh installs only). Should never fire in production. | Contact support if you see this — internal config bug. |
503 | service_unavailable | API process unhealthy. Usually transient. | Retry with backoff. Check /status. |
404 | not_found | Resource lookup miss (e.g. `/v1/plagiarism/scans/:id` with an unknown id). | Double-check the id. Ids are UUIDs returned by the originating endpoint. |
Retry-safety matrix
| HTTP | Retry? | Strategy |
|---|---|---|
400 | No | Fix the request and re-send. |
401 | No | Rotate / regenerate the key. |
402 | No | Upgrade or change the input. |
413 | No | Shrink the body. |
422 | No | Provide more text. |
429 | Yes | Wait until resets_at or honor Retry-After. |
500 / 502 / 503 | Yes | Exponential backoff with jitter, cap at 3 retries. |
The official SDKs implement the right retry strategy for every code — you don't need to think about this if you use them.