Skip to content

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.

StatusCodeMeaningRetry
401unauthorizedMissing, unknown, expired or revoked key — including revoked during the request.Not with the same key.
403forbiddenValid key, but wrong scope, or a workspaceId that is not the key’s.No.
404not_foundNo such route — and the deliberately opaque /health route with a query delimiter (including bare ?) or a non-GET method.No.
405method_not_allowedWrong method. Applies to the /v1 routes and to /mcp. Allow names the right one.No.
413request_too_largeREST bodies cap at 4 KiB, MCP at 16 KiB.No — send less.
415unsupported_media_type / unsupported_encodingBody must be application/json, Content-Encoding: identity. MCP returns json_required for both.No.
400invalid_jsonThe body is absent, not valid JSON, or not valid UTF-8. MCP returns invalid_request instead.No.
400invalid_queryA URL query delimiter (including bare ?) was present on an original /v1 route or /mcp; pilot routes use invalid_request.No.
503auth_unavailableThe authorization store could not be reached. Applies to the original REST and MCP routes; pilot routes return automation_unavailable.Yes, with backoff.
StatusCodeMeaningRetry
400invalid_queryUnsupported body: unknown field, malformed date, window outside 1–31 days, or a metric/dimension other than ga4.sessions by date.No.
403forbiddenThe body’s workspace is not the key’s.No.
404connection_unavailableNo active connection for this workspace, connection and property — or it was disconnected mid-read.Only after reconnecting.
409data_not_readyNothing in the window is published. missingDates lists the window.After a successful sync.
409data_incompleteSome days are published, some are not. missingDates names them.Narrow the window, or sync and retry.
409timezone_conflictPublished dates use different property timezones. No analytics read occurs.Re-sync every requested date using the property’s current timezone.
503query_unavailableA 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.

CodeMeaning
provider_auth_failedThe stored credential was rejected. Reconnect.
provider_unavailable / provider_errorThe provider failed or was unreachable.
malformed_report / malformed_metadata / malformed_row / unexpected_schemaThe response did not match the expected shape.
restricted_reportThe 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_reasonThe provider reported an empty result with a reason.
changing_reportThe result changed between pages — it cannot be treated as one snapshot.
row_count_limit / row_count_mismatch / page_limitRow or page bounds were exceeded or disagreed.
incomplete_report / incomplete_extraction / invalid_extractionThe extraction did not complete.
duplicate_dateThe provider returned a date twice.
raw_integrity_failure / raw_identity_mismatch / raw_missing / raw_commit_failedStored raw evidence failed its checksum or identity check, or could not be written.
analytics_load_failedThe 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:

StatusCodeCause
401unauthorizedMissing, unknown, expired or revoked key after the request reaches the pinned private URL/Host/Origin boundary.
403forbiddenA disallowed URL, Host or Origin (checked before authentication), or a key lacking query:read.
405method_not_allowedNot POST.
406JSON-RPC -32000Accept did not explicitly allow both application/json and text/event-stream.
415json_requiredCovers both halves REST splits into unsupported_media_type and unsupported_encoding.
413request_too_largeOver 16 KiB.
400invalid_queryA URL query delimiter was present, including a bare ?.
400invalid_requestBody is not bounded, valid JSON. REST returns invalid_json.
503query_unavailable / auth_unavailableExchange 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 codeMeaning
-32600Invalid request — malformed envelope, or an id that is not a bounded string or non-negative integer.
-32601Unsupported request method when the JSON-RPC object carries an id.
-32602Unsupported tool, or arguments that fail the tool’s strict schema.
-32603Internal 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.

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.

StatusCodeAction
400invalid_request, invalid_json, invalid_scheduleUse the closed body for that route. Only 60 or 1440 minutes and 1–31 dates are supported.
403source_permission_required, destination_permission_requiredRestore the corresponding runtime grant.
403schedule_enable_forbidden, schedule_inactiveCheck current connection, destination and schedule authorization.
404destination_not_found, schedule_not_found, pipeline_not_found, connection_unavailableCheck identifiers in the key’s own workspace.
409timezone_unverified, timezone_conflictPublish source data in one verified property timezone before configuring or enabling.
409destination_creation_unconfirmedReview the creation outcome with an operator. Setup will not blindly create another workbook.
409destination_conflict, destination_unavailable, schedule_conflict, redrive_conflictCheck current state; one schedule may be enabled per destination, and redrive requires a failed occurrence.
409sheet_capacity_exhausted, sheet_layout_conflictResolve capacity or restore the reserved workbook layout.
502provider_auth_failed, provider_unavailableReconnect a rejected grant or investigate provider availability.
503credential_configuration_unavailable, automation_configuration_unavailable, credential_unavailable, credential_busy, provider_rate_limited, automation_unavailableResolve 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.

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.