Errors
Success and failure share one envelope, so a client parses one shape.
{ "success": true, "data": { "...": "..." } }{ "success": false, "data": null, "error": "Date range cannot exceed 90 days" }success is the field to branch on. error is a short human-readable sentence — display it, log it, but do not parse it: the wording can change, the status code will not.
Status codes
| Status | Meaning | Retry? |
|---|---|---|
200 | The call succeeded | — |
401 | Token missing, invalid, or revoked | No. Re-issue the token |
422 | A parameter is invalid — a date range over 90 days, a missing required parameter | No. Fix the request |
429 | Over the rate limit | Yes, after Retry-After |
500 | Something failed on our side | Yes, with backoff |
503 | Your token could not be checked — a transient failure, not a credential problem | Yes, after Retry-After |
No endpoint returns 403, 404 for a missing offer, or a partial success. A path that is not one of the eleven is not an API answer at all — it never reaches the envelope, so parse success only on the paths documented here.
422. start and end that cannot be parsed are ignored and the default window runs instead, returning 200 — see Date ranges. The only parameter errors that reach 422 are a range over 90 days and a missing required parameter.An offer that does not exist
GET /data/offer/{id} with an id that is not yours — or no longer exists — returns 200 with data: null, not 404.
{ "success": true, "data": null }data: null as "not found", and branch on it before reading fields.A deleted offer inside a ranking
/data/analytics/top ranks from claim history, so an offer deleted after it earned still appears. Those rows carry isDeleted: true and no url, because there is no page left to open.
Retrying safely
Every endpoint is a GET and nothing here changes state, so retries are always safe — no idempotency key, no duplicate-write risk.
That said, retry only what can succeed:
async function read(path, params, attempt = 1) {
const response = await call(path, params);
const body = await response.json();
if (body.success) return body.data;
if (response.status === 429 && attempt <= 3) {
const wait = Number(response.headers.get('Retry-After') || 60);
await new Promise(r => setTimeout(r, wait * 1000));
return read(path, params, attempt + 1);
}
if (response.status >= 500 && attempt <= 3) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
return read(path, params, attempt + 1);
}
throw new Error(body.error || `HTTP ${response.status}`);
}A 401 or 422 retried is a 401 or 422 again. Surface it instead.
Messages you may meet
| Message | Status | Cause |
|---|---|---|
Missing access token | 401 | No Authorization header, or not Bearer <token> |
Invalid access token | 401 | Token is wrong, rotated away, or revoked |
Service unavailable | 503 | The token could not be checked. Retry after Retry-After — do not rotate |
Date range cannot exceed 90 days | 422 | Shorten the range |
placement is required — priority is ranked within one placement | 422 | /data/offers/by-priority called without placement |
Rate limit exceeded | 429 | Wait for Retry-After |
read_failed | 500 | A read failed on our side. This one is a fixed code, not a sentence — the detail stays in our logs so a message can never leak a host or a query. Retry with backoff |