Reference
Errors
Every public error code, what it means, and whether retrying will help.
One envelope, everywhere:
{ "error": { "code": "data_not_ready", "message": "No complete data has been published for this window." } }Match on code. Messages are fixed strings chosen per code and may be reworded;
they never carry SQL, provider text, ciphertext or reflected header values, so they
will not tell you more than the code does.
data_not_ready and data_incomplete add missingDates. The 409
timezone_conflict response has only code and message.
Note that /mcp uses its own codes for two of these conditions. They are listed
separately below; do not assume a REST code applies there.
Authentication and transport (REST)
Section titled “Authentication and transport (REST)”| Status | Code | Meaning | Retry |
|---|---|---|---|
| 401 | unauthorized | Missing, unknown, expired or revoked key — including revoked during the request. | Not with the same key. |
| 403 | forbidden | Valid key, but wrong scope, or a workspaceId that is not the key’s. | No. |
| 404 | not_found | No such route — and the deliberately opaque /health route with a query delimiter (including bare ?) or a non-GET method. | No. |
| 405 | method_not_allowed | Wrong method. Applies to the /v1 routes and to /mcp. Allow names the right one. | No. |
| 413 | request_too_large | REST bodies cap at 4 KiB, MCP at 16 KiB. | No — send less. |
| 415 | unsupported_media_type / unsupported_encoding | Body must be application/json, Content-Encoding: identity. MCP returns json_required for both. | No. |
| 400 | invalid_json | The body is absent, not valid JSON, or not valid UTF-8. MCP returns invalid_request instead. | No. |
| 400 | invalid_query | A URL query delimiter (including bare ?) was present on an original /v1 route or /mcp; pilot routes use invalid_request. | No. |
| 503 | auth_unavailable | The authorization store could not be reached. Applies to the original REST and MCP routes; pilot routes return automation_unavailable. | Yes, with backoff. |
| Status | Code | Meaning | Retry |
|---|---|---|---|
| 400 | invalid_query | Unsupported body: unknown field, malformed date, window outside 1–31 days, or a metric/dimension other than ga4.sessions by date. | No. |
| 403 | forbidden | The body’s workspace is not the key’s. | No. |
| 404 | connection_unavailable | No active connection for this workspace, connection and property — or it was disconnected mid-read. | Only after reconnecting. |
| 409 | data_not_ready | Nothing in the window is published. missingDates lists the window. | After a successful sync. |
| 409 | data_incomplete | Some days are published, some are not. missingDates names them. | Narrow the window, or sync and retry. |
| 409 | timezone_conflict | Published dates use different property timezones. No analytics read occurs. | Re-sync every requested date using the property’s current timezone. |
| 503 | query_unavailable | A backend is unreachable, or a stored row failed an integrity check. | Yes, with backoff. |
query_unavailable deliberately covers both “the database is down” and “the data did
not pass its consistency checks”. The boundary fails closed rather than returning a
result it cannot vouch for, and does not distinguish the two to the caller.
400 — the request is wrong. invalid_json, invalid_request, invalid_window,
invalid_idempotency_key, invalid_provider_request, and incomplete_day when the
window includes a day still in progress in the property’s timezone; invalid_query
means the URL carried a query delimiter. Do not retry unchanged.
404 — polling run not found. run_not_found when GET /v1/syncs/{runId}
cannot find that run in the key’s workspace. Router-level not_found is separate.
409 — conflict. idempotency_conflict (that key was used for a different
window — use a new key), lease_lost (another worker took over), or
publication_rejected. window_in_progress means another key owns the exact
window’s live lease; retry the same key after that owner finishes or expires.
500 — misconfiguration. invalid_lease_config: the sync service’s lease duration
is outside the permitted range. Classified and documented for completeness, but the
deployed service does not override the default lease, so this should not appear; if
it does, it needs an operator, not a retry.
502 — upstream data could not be trusted. The run published nothing.
| Code | Meaning |
|---|---|
provider_auth_failed | The stored credential was rejected. Reconnect. |
provider_unavailable / provider_error | The provider failed or was unreachable. |
malformed_report / malformed_metadata / malformed_row / unexpected_schema | The response did not match the expected shape. |
restricted_report | The provider declared active metric restrictions on the request — the metric itself was refused. Note this is not thresholding: a thresholded report is accepted and produces a partial run that publishes nothing. |
empty_report_reason | The provider reported an empty result with a reason. |
changing_report | The result changed between pages — it cannot be treated as one snapshot. |
row_count_limit / row_count_mismatch / page_limit | Row or page bounds were exceeded or disagreed. |
incomplete_report / incomplete_extraction / invalid_extraction | The extraction did not complete. |
duplicate_date | The provider returned a date twice. |
raw_integrity_failure / raw_identity_mismatch / raw_missing / raw_commit_failed | Stored raw evidence failed its checksum or identity check, or could not be written. |
analytics_load_failed | The load into the analytics store failed. |
Most 502s are worth one retry with the same idempotency key;
provider_auth_failed and the malformed_* family are not, since they will repeat
until something changes.
503 — temporarily unavailable. sync_unavailable, auth_unavailable,
credential_unavailable, credential_configuration_unavailable (the credential
keyring could not be parsed — needs an operator, not a retry), credential_busy
(another run holds the credential lease), provider_rate_limited,
ledger_unavailable, analytics_load_unavailable, invalid_clickhouse_config.
Retry with backoff, same key.
If the gateway receives a response it cannot classify, it returns
503 sync_unavailable rather than forwarding an unknown code. An unrecognised code
stored on an old run is replaced with sync_unavailable when read back.
The allowlist is operation-specific: polling does not release create-only extraction
or contention failures, and create does not release run_not_found. json_required
is classified for create but should not arise from the current gateway/Worker pairing.
Transport failures use the same HTTP statuses as REST but not always the same codes:
| Status | Code | Cause |
|---|---|---|
| 401 | unauthorized | Missing, unknown, expired or revoked key after the request reaches the pinned private URL/Host/Origin boundary. |
| 403 | forbidden | A disallowed URL, Host or Origin (checked before authentication), or a key lacking query:read. |
| 405 | method_not_allowed | Not POST. |
| 406 | JSON-RPC -32000 | Accept did not explicitly allow both application/json and text/event-stream. |
| 415 | json_required | Covers both halves REST splits into unsupported_media_type and unsupported_encoding. |
| 413 | request_too_large | Over 16 KiB. |
| 400 | invalid_query | A URL query delimiter was present, including a bare ?. |
| 400 | invalid_request | Body is not bounded, valid JSON. REST returns invalid_json. |
| 503 | query_unavailable / auth_unavailable | Exchange failed or deadline exceeded; authorization store unreachable. |
Protocol failures are different: they are returned with HTTP 200 and a JSON-RPC
error object, because that is what the protocol requires. A 200 from /mcp does not
mean the call succeeded.
| JSON-RPC code | Meaning |
|---|---|
-32600 | Invalid request — malformed envelope, or an id that is not a bounded string or non-negative integer. |
-32601 | Unsupported request method when the JSON-RPC object carries an id. |
-32602 | Unsupported tool, or arguments that fail the tool’s strict schema. |
-32603 | Internal error, and the fallback the boundary substitutes when a relayed frame carries no usable code. When it sanitises a diagnostic it keeps the original code and replaces only the message. |
Tool-level failures come back as a result with isError: true carrying the same
fixed error envelope, including bounded missingDates for data_not_ready and
data_incomplete, so a query failure inside MCP reads the same as it does over REST.
A valid notification omits id, receives an empty HTTP 202, and never receives a
JSON-RPC error. Unknown notification methods and id-less tools/call are also
acknowledged this way without dispatching a tool.
Controlled pilot automation
Section titled “Controlled pilot automation”These routes are implemented and locally tested; new live scheduled delivery and failure/recovery acceptance remain pending. A failed source or export is visible in the schedule status, separately from the last successful delivery.
| Status | Code | Action |
|---|---|---|
| 400 | invalid_request, invalid_json, invalid_schedule | Use the closed body for that route. Only 60 or 1440 minutes and 1–31 dates are supported. |
| 403 | source_permission_required, destination_permission_required | Restore the corresponding runtime grant. |
| 403 | schedule_enable_forbidden, schedule_inactive | Check current connection, destination and schedule authorization. |
| 404 | destination_not_found, schedule_not_found, pipeline_not_found, connection_unavailable | Check identifiers in the key’s own workspace. |
| 409 | timezone_unverified, timezone_conflict | Publish source data in one verified property timezone before configuring or enabling. |
| 409 | destination_creation_unconfirmed | Review the creation outcome with an operator. Setup will not blindly create another workbook. |
| 409 | destination_conflict, destination_unavailable, schedule_conflict, redrive_conflict | Check current state; one schedule may be enabled per destination, and redrive requires a failed occurrence. |
| 409 | sheet_capacity_exhausted, sheet_layout_conflict | Resolve capacity or restore the reserved workbook layout. |
| 502 | provider_auth_failed, provider_unavailable | Reconnect a rejected grant or investigate provider availability. |
| 503 | credential_configuration_unavailable, automation_configuration_unavailable, credential_unavailable, credential_busy, provider_rate_limited, automation_unavailable | Resolve configuration or permission problems; retry transient failures with backoff. |
Schedule status preserves fixed operational codes such as attempts_exhausted,
retry_after_exceeds_policy, destination_capacity_exhausted,
source_not_fully_published, property_timezone_changed,
sheet_verification_failed, export_query_forbidden, export_query_superseded and
export integrity failures.
These are status fields, not additional HTTP status codes. Unknown stored text is
replaced with automation_unavailable; provider bodies and credentials never appear.
Retry etiquette
Section titled “Retry etiquette”There are no rate limits yet. That is not permission to hammer the API — a sync retry loop without backoff spends provider quota, and GA4’s quota is per property, not per caller. Use the same idempotency key so retries resume rather than duplicate.