Docs/Wire contract (v1)

Rate limits & errors

The SDK must tolerate all of this without crashing. The normalized result for any non-200 or unfillable response is simply no-fill: render nothing, optionally retry with backoff.

Status codes & body shapes

NameTypeRequiredNotes
No fill200Optional{"fill": false}reason)
Missing slot400Optional{"fill": false, "error": "slot_id or slot_name is required"}
Handler rate limit429Optional{"fill": false, "error": "rate limit exceeded"} + Retry-After: 60
Middleware rate/DDoS limit429Optional{"error": "..."} (no fill key) + maybe Retry-After
WAF / SQLi block403Optional{"error": "Request blocked due to security policy"} — see WAF-safe encoding
Malformed query400OptionalBad UUID, missing api_key — body is plain text, not JSON

Rate limit thresholds

Per client IP unless otherwise noted:

  • 200 req / 10s — DDoS-tier middleware limit
  • 1000 req/min per IP
  • 100 req/min per api_key (GET)
  • plus a per-IP RATE_LIMIT_PER_MINUTE (default 60) in the serve handler itself

SDKs default to client-side throttling — a minimum interval between requests for the same slot, and a refresh interval floor of 30s (see Beacons & lifecycle) — to stay well under all of these.

The parsing rule: fail closed

Never assume the error body is JSON

On any status ≥ 400, or a body that doesn't parse as JSON, or fill != true, return the normalized NoFill result. Surface the HTTP status and Retry-After on the result object so callers can implement backoff — but the default render path treats everything unfillable as "show nothing." An ad request must never throw into the host app, crash a page, or block content.

Every official SDK implements this identically: a network error, a timeout, a non-200 response, or an unparseable body all collapse to the same { filled: false } result your rendering code already handles for the ordinary no-fill case — there is no separate error path to write.