Skip to content

Guide

Authentication

Bearer API keys, the two scopes, and what happens when a key is revoked mid-request.

Every route except GET /health requires a bearer API key:

Authorization: Bearer <key>

Keys are verified with HMAC-SHA-256 against authoritative control-plane records on every request. There is no cached validation and no token exchange, so revocation and expiry take effect immediately.

Keys are issued by an operator. There is no signup, no dashboard and no key management API yet — during the controlled staging phase, a key is created for a specific workspace with a specific scope and a short expiry, and revoked when the work it was issued for is done.

If you have a key, you were given the workspace and connection identifiers with it. You cannot discover them through the API.

Two scopes exist, and they are genuinely separate — one does not imply the other.

ScopeGrants
query:readPOST /v1/query, GET /v1/metrics/ga4.sessions, POST /mcp, GET /v1/pilot/schedules/{scheduleId}
sync:writePOST /v1/syncs, GET /v1/syncs/{runId}, all four POST /v1/pilot/... operations

A sync:write key can trigger extraction but cannot read the resulting data. A query:read key can read published data but cannot cause a provider call. Issue the narrower one: a key that only ever reads should not be able to spend provider quota.

Using a key outside its scope returns 403 forbidden, not 401 — the key is valid, the operation is not permitted.

The key determines the workspace. workspaceId in a request body must equal the key’s own workspace; a mismatch is 403 forbidden, never a cross-tenant read.

Workspace headers are ignored entirely. There is no header, parameter or body field that lets a caller act on a workspace other than its key’s.

The two surfaces differ, which matters when you are reading a status code.

On the /v1 routes, the route match, the method check and the query-delimiter check all run first. GET /v1/query is 405 and POST /v1/query?x=1 is 400, whether or not your key is valid.

On /mcp, the pinned URL, Host and optional Origin are checked first, before the authorization store is touched. A disallowed origin is therefore 403 even when the request also carries a bad key. Authentication follows for requests that reach that private boundary.

Worth knowing if you are reasoning about what a leaked key could reach: authorization is checked before the backend read and again before the response is released.

If a key is revoked, expires, or is reassigned to another workspace while a query is in flight, the request returns 401 and the data is discarded. A read that began while the key was valid does not complete just because it started in time. The same recheck applies to sync responses and to MCP tool results.

Pilot requests are authenticated again by the private sync Worker, then rechecked before the gateway releases either a result or a scoped error. Pilot request bodies do not accept workspaceId; the current key supplies it.

An enabled, versioned schedule and active source/destination grants authorize background work. The runtime does not save or replay the configuring API key. Disabling a schedule invalidates unfinished source and export ownership; requests already sent to Google cannot be recalled. See Pilot automation.

If Google returns a valid replacement refresh token immediately before that schedule authority is lost, the broker retains the encrypted replacement under its separate exact credential lease. It still discards the data response and does not start a later Google request for the denied schedule.

If the authorization store cannot be reached, the original REST and MCP routes return 503 auth_unavailable; pilot routes return 503 automation_unavailable. An unreachable key store never results in an unauthenticated read. Retry with backoff.

Authentication failures return fixed messages. unauthorized does not distinguish between a key that never existed, one that expired, and one that was revoked, and error bodies never carry SQL, provider text, ciphertext or reflected header values.

This is deliberate, and it means the API cannot be used to probe which keys or workspaces exist. When debugging, work from the code and your own records rather than expecting the message to narrow it down.

REST bodies must be application/json, at most 4 KiB, unencoded (Content-Encoding other than identity is rejected).

Any URL query delimiter, including a bare ?, is rejected on every /v1 route and on /mcp — keys and query payloads do not belong in the URL. Run and schedule identifiers do appear in documented resource paths. /health is the exception in mechanism rather than spirit: it matches only an exact GET with no query delimiter; a delimiter or another method still matches that deliberately opaque route and returns 404 not_found rather than 400 invalid_query.

Responses are no-store, nosniff and noindex, nofollow, and there is no CORS policy: this is not a browser-callable API.