Errors
One error envelope, one stable code per failure, and a matching HTTP status.
Every error uses the same envelope:
{
"error": {
"code": "invalid_request",
"message": "Body must be JSON.",
"request_id": "req_8fK2mQ"
}
}codeis a stable identifier. Branch on it.messageis human-readable and safe to log, but do not branch on it.request_idmatches theX-Request-Idresponse header. Paste the JSON into a support request and it is enough to find the call.
The HTTP status is derived from code, never inferred from the message.
Codes
| Status | code | Meaning |
|---|---|---|
400 | invalid_request | The query or body failed the operation's schema, the body was not a JSON object, an Idempotency-Key was too long, or a key was reused with a different body. |
401 | unauthorized | Missing, invalid, revoked or expired key, or an OAuth token sent to REST. |
403 | forbidden | The key is valid but short on scope or the approval grant. The body carries required_scope or required_grant, plus key_scope. See Authentication. |
404 | not_found | No such resource in this workspace. |
409 | conflict | A request with this Idempotency-Key is still running. |
422 | proposal_rejected | Well-formed, but refused by the product's rules or by Meta. message says why. |
429 | rate_limited | Too many requests. See Rate limits. |
502 | upstream_error | An upstream provider was unreachable or errored. |
500 | internal | An unexpected server error. The message is fixed and never contains internal detail. Quote the request_id. |
What a 422 usually means
A tool that refuses returns 422 proposal_rejected with a message that says why. The
common causes are:
- A launch refused before upload: an ad name that does not match
id__locale__format__persona__angle__concept__variant, or a form ad set mixed with a website destination. - Meta refused the call, for example a missing permission or a throttled quota.
- The free trial has ended. Launching, turning ads on and changing live ads are off until a plan is active. The message links to Settings → Billing. Pausing, reading and everything else still work.
- A frozen record: a second Conclusion on a Test, or a supported or rejected
Conclusion that contradicts the latest Observation without an
overrideReason.
A 422 is a policy outcome, not a bug. The request was well-formed and was refused on
purpose, so handle it separately from 400.
Some operations answer 404 not_found for a missing row instead of 422: reading or
updating a creative, reading its recipe, reading a brief, a report view, a Test or a
learning, and updating a copy line. Each operation in the reference lists a 404 when it
can return one.
Retrying
429: wait forRetry-Afterand retry.500,502or a timeout: retry with the sameIdempotency-Key. See Idempotency.400,401,403,404,422: fix the request. Retrying unchanged will fail again.