Developer

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:

  1. Every workspace must have at least one embed instance (badge or widget) created via the Console (/developer/workspaces/{id}/badges).
  2. 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.
  3. 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:

  1. Open your site in a fresh browser tab; confirm the page actually loads the widget.js script (check DevTools → Network for widget.js and heartbeat).
  2. If the script is missing: re-paste the snippet from /developer/workspaces/{id}/badges/{embed_id} and redeploy your site.
  3. If the script is present but blocked (CSP / ad blocker / Cross-Origin-Opener-Policy), allow zippfeed.com in your security headers.
  4. Once any heartbeat lands, the sweeper flips status to ok on 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.