4. Response shape
4.1 Success — JSON body, status 2xx
4.2 Error envelope — every 4xx/5xx
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Human-readable explanation.",
"field": "category"
}
}
field is optional (set when the error pins to a specific input).
4.3 Status code map
| Status | Code | Trigger |
|---|---|---|
| 401 | NOT_AUTHENTICATED |
Missing / invalid Authorization header |
| 403 | BADGE_MISSING |
Workspace has no badge heartbeat in 72h |
| 403 | WORKSPACE_SUSPENDED |
Admin-suspended workspace |
| 404 | NOT_FOUND |
Resource doesn't exist or not visible to this key |
| 409 | INVALID_STATE |
State machine collision (mostly admin endpoints) |
| 422 | VALIDATION_FAILED |
Bad input (typed enum, missing field, etc.) |
| 429 | RATE_LIMITED |
Per-minute rate limit hit; includes retry_after_sec |
| 429 | QUOTA_EXHAUSTED |
Daily quota exhausted; Retry-After: 86400 |
429 responses always include Retry-After: <seconds> header.
4.4 BADGE_MISSING — what it means and how to recover
BADGE_MISSING is the most common 403 partners hit. It's a soft-fail
state — your key is valid, your workspace is approved, but we can't
see your site loading our embed snippet, so the read API gates itself
off until you fix the integration.
How the check works:
- Every workspace must have at least one embed instance (badge or
widget) created via the Console (
/developer/workspaces/{id}/badges). - The embed snippet you paste on your site is a one-line
<script src=".../widget.js" data-embed-id="bdg_…" async>tag. It loads a tiny iframe that pings our heartbeat endpoint on every page view it appears on. - A sweeper recomputes workspace badge status hourly:
| Last heartbeat | Status | Read API |
|---|---|---|
| Within 48h | ok |
✅ Serves normally |
| 48h–72h | warning |
✅ Serves normally (grace period) |
| >72h | missing |
❌ 403 BADGE_MISSING |
| Never | unknown |
❌ 403 BADGE_MISSING |
Recovery — within the hour:
- Open your site in a fresh browser tab; confirm the page actually
loads the
widget.jsscript (check DevTools → Network forwidget.jsandheartbeat). - If the script is missing: re-paste the snippet from
/developer/workspaces/{id}/badges/{embed_id}and redeploy your site. - If the script is present but blocked (CSP / ad blocker /
Cross-Origin-Opener-Policy), allowzippfeed.comin your security headers. - Once any heartbeat lands, the sweeper flips status to
okon its next tick (≤1 hour). The API resumes serving immediately after that tick.
The /developer/onboarding page surfaces your live heartbeat status
in step 5 — that's the fastest way to confirm recovery without
hitting the API blind.
4.5 Example error responses
Concrete payloads partners hit most often. Every body matches the envelope from §4.2; relevant headers shown next to each.
401 — missing or invalid Bearer token
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"error": {
"code": "NOT_AUTHENTICATED",
"message": "Missing or invalid Authorization header."
}
}
422 — unknown enum value
Request: GET /api/v1/developer/posts?sentiment=BLOOP
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Unknown sentiment value: BLOOP. Expected one of BULLISH, NEUTRAL, BEARISH.",
"field": "sentiment"
}
}
429 — per-minute rate limit hit
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 17
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1747923617
{
"error": {
"code": "RATE_LIMITED",
"message": "Per-minute rate limit exceeded.",
"retry_after_sec": 17
}
}
429 — daily quota exhausted
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 41782
X-Quota-Limit: 5000
X-Quota-Remaining: 0
X-Quota-Reset: 1747958400
{
"error": {
"code": "QUOTA_EXHAUSTED",
"message": "Daily quota of 5000 requests exhausted. Resets at next UTC midnight.",
"retry_after_sec": 41782
}
}
For 429 responses, prefer the Retry-After header over the body —
it's standards-track and most HTTP clients honour it automatically
when you wire retry middleware.