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.
{ "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_request400 invalid_json401 unauthorized403 forbidden404 not_found429 rate_limited409 data_not_ready500 internal_errorWhy 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.
{ "error": { "code": "data_not_ready", "message": "No stored forecast for this brand and type yet" }}- A
500would say we broke. We did not: the request was well formed and authorised. - A
200with zeroes would be a lie, and worse, a lie your chart would render as a flat line somebody then acts on. - A
404would 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
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.
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.