Skip to content

Core concepts

Errors

Every failure on this API uses one envelope and one of eight codes. The HTTP status tells your transport layer what happened; the code tells your application what to do about it.

The envelope

Errors always have this shape. There is no variant with a bare string, no variant with an array of errors, and no 200 that carries a failure in the body.

400
{  "error": {    "code": "invalid_request",    "message": "level: Invalid enum value. Expected 'account' | 'campaign' | 'adset' | 'ad'"  }}

A validation message names the field that failed and what was expected, because the alternative is a developer bisecting their own query string.

Codes

400 invalid_request
A parameter failed the schema: wrong type, out of range, or an enum value that does not exist. The message names the field.
400 invalid_json
The request body was not parseable JSON.
401 unauthorized
No Authorization header, a malformed one, or a key that has been revoked or has expired.
403 forbidden
The key is valid but is not scoped to the brand in the request. Keys can be narrowed to one brand.
404 not_found
No such resource for this key.
429 rate_limited
The per-minute quota for this key is spent. Retry-After and error.retry_after both carry the seconds to wait.
409 data_not_ready
The request was well formed and authorised, and there is no ingested data to answer it yet. A 500 would say we broke and a 200 of zeroes would be a lie, so this is its own status.
500 internal_error
Something failed on our side. The request_id identifies the call.

Why missing data is a 409

This is the one that surprises people, so it is worth the paragraph. A request for a forecast that does not exist yet is not a client error, not a server error, and not an empty success.

409
{  "error": {    "code": "data_not_ready",    "message": "No stored forecast for this brand and type yet"  }}
  • A 500 would say we broke. We did not: the request was well formed and authorised.
  • A 200 with zeroes would be a lie, and worse, a lie your chart would render as a flat line somebody then acts on.
  • A 404 would say the resource does not exist, when what is true is that it does not exist yet.

Handle 409 as a state, not an exception

Forecasts and budget recommendations are produced on a schedule from accumulated history. A brand connected this morning genuinely has none. Rendering that as “not enough history yet” is both true and more useful than an error toast.

Handling errors

A minimal client

Three branches cover everything: retry a 429 after the interval the response names, treat a 409 as a state, and raise the rest.

TypeScript
const res = await fetch(url, { headers }); if (res.status === 409) {  // Well formed, authorised, and there is nothing to answer with yet.  // Show "not enough history" rather than a zero or an error.  return { state: "not_ready" };} if (res.status === 429) {  const wait = Number(res.headers.get("Retry-After") ?? 1);  await sleep(wait * 1000);  return retry();} if (!res.ok) {  const { error } = await res.json();  throw new Error(`${error.code}: ${error.message}`);}

Reporting a problem

Every successful response carries meta.request_id and every error is logged against the same id. Quoting it in a support conversation is the difference between us finding the call and us asking you what time it was.