Every failure answers in one envelope, and every code below is a link a client can follow.
Every error from the REST API and from the MCP tools is the same shape: a code you can branch on, a message written to be acted on, and a doc_url pointing at the section below. Codes carrying extra context add fields alongside those three: required_scope, fields, the budget limits, retry_after and request_id, as the table says.
{
"error": {
"code": "budget_cap_exceeded",
"message": "A single API call may change a budget by at most 25%. Allowed range for this campaign: 30.00 to 50.00.",
"doc_url": "https://glitchads.ai/docs/errors#budget_cap_exceeded",
"allowed_min": 30.0,
"allowed_max": 50.0
}
}
Messages name the field, the missing scope or the allowed range, so a client can correct itself without a second request to find out what went wrong.
Three answers use other shapes. The OAuth endpoints answer in the standard OAuth error format, error and error_description. A path that does not exist answers a plain 404. Errors the MCP server raises itself, such as a missing required argument, use the envelope without a doc_url.
| Code | HTTP | What it means |
|---|---|---|
| HTTP401 | Missing, invalid or expired credentials. With OAuth, refresh the access token, and authorize again only if the refresh fails. With an API key, check the header, that the key is active and unexpired, and that the person who created it is still a member with a role that covers its scopes. | |
insufficient_scope |
HTTP403 | The credential lacks the scope named in required_scope. Reconnect approving it, or create a key that carries it. |
org_access_denied |
HTTP403 | The credential cannot act in that organization, or no organization has that slug. The two are answered the same way on purpose. |
role_denied |
HTTP403 | Your role in the organization does not allow this operation; the audit export is Owner only. A Member attempting a write sees insufficient_scope instead, because writes are outside a Member credential’s scope ceiling. |
not_found |
HTTP404 | Unknown campaign, recommendation, action, report, business profile or task, including an id that belongs to another organization. The message names which one. |
validation_error |
HTTP400 | A bad parameter. The message names the accepted values, and fields lists the parameters at fault. |
budget_cap_exceeded |
HTTP422 | The change would breach a budget limit. Past the per-change limit, allowed_min and allowed_max give the range; past the organization's monthly cap, monthly_cap and allowed_max_daily give the room left. |
approval_required |
HTTP202 | The organization requires a person to approve API and MCP writes, so the change is waiting rather than done. queued_recommendation_id names it: poll /v1/queued-writes/{id} to learn whether it was approved, rejected or failed. Reads and dry runs are never held. |
sync_conflict |
HTTP409 | Launching the campaign was blocked because it was changed directly in Google Ads since Glitch last synced it. Sync it from the dashboard, then retry. |
rate_limited |
HTTP429 | Too many requests. Wait for Retry-After, also given as retry_after. |
task_failed |
HTTP500 | A write failed while executing, for example because Google Ads rejected it. The message carries the reason, and the attempt counts against the daily write quota. |
internal_error |
HTTP500 | Our fault. Retry with backoff; if it persists, contact support with the request_id, which is also the X-Request-ID response header. |
Requests are counted per credential, and writes per organization per day. Both are set by your plan. Every authenticated REST response carries the headers, errors included, so a client never has to guess how much room is left. A request refused as unauthorized never reaches the limit, and MCP tool results carry no HTTP headers: read retry_after from the error instead.
| Requests a minute | Writes a day | |
|---|---|---|
| Standard plans | Requests a minute60 | Writes a day200 |
| Larger plans | Requests a minute240 | Writes a day1000 |
# a request that went through
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 41
X-RateLimit-Reset: 19
# a request refused with rate_limited
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
Retry-After: 1
X-RateLimit-Reset is the number of seconds until the allowance is full again. Larger plans can burst to 480 requests, so X-RateLimit-Remaining can be higher than the limit.
A refused request answers rate_limited with Retry-After in seconds. Wait at least that long. Another request can take the freed allowance first, so be ready to wait again.
The daily write quota exists so that a misbehaving agent cannot rewrite an account wholesale. It is shared by every credential in the organization and refills gradually through the day, not at midnight. Every write that reaches execution counts, including one that then fails, and generating a report counts as a write. Reads never count against it, and neither do dry runs or undoing an action that was already undone.
Safe to retry: every read, glitch_undo_action, which is idempotent, and any call made as a dry run.
Safe to retry with the same parameters: an executed write that answered rate_limited or internal_error before it confirmed anything.
Not safe to repeat: a write that already returned an action_id. It succeeded, and sending it again performs it twice. Reverse it with an undo instead. Writes that continue in the background return a task_tracking_id; polling that is safe, but re-sending the original request starts a second job.
When a response carries Retry-After, wait that long; the daily write quota can ask for several minutes. Otherwise back off exponentially from one second, cap at sixty, and add jitter.
Work that takes longer than a request answers 202 Accepted with a task_tracking_id rather than holding the connection open. Campaign creation, ad creation and report generation work this way. A state change answers straight away with both a task_tracking_id and an action_id.
Poll the task until its status is completed or failed. A failed task carries an error with a reason and a message saying what to do next, and the work can be started again.