API reference

Search Console

Pull Google Search Console data (clicks, impressions, CTR, average position, queries, pages, countries, devices) into a site's dashboard. Synced into Totallytics storage, so history outlives Google's 16-month retention.

Base URL: https://analytics.bitgate.dev. Download OpenAPI or read this page as Markdown.

Access

All endpoints on this page require the site owner Firebase ID bearer token, even for public sites — GSC data never appears on public dashboards. The OAuth callback (/api/import-oauth/google/callback) is public; the signed, encrypted, 10-minute-expiring state blob is the capability.

Connecting

The OAuth dance is browser-driven and normally starts from site settings → Google Search ConsoleConnect. Programmatically:

  1. POST /api/import-oauth/google/start with { "site": "example.com", "provider": "google-search-console", "origin": "https://totallytics.app" } → returns { "url" }; send the browser there. origin must be one of the app hosts and decides where the user lands afterwards.
  2. Google redirects back to /api/import-oauth/google/callback, which stores an encrypted refresh token (one connection per account + provider) and bounces to …/settings?gsc=connected.
  3. GET /api/sites/{hostname}/gsc/properties?connection_id=… lists the account's Search Console properties, with matches: true on the ones covering this site.
  4. POST /api/sites/{hostname}/gsc/link with { "connection_id", "property" } validates access and starts the backfill.

Sync behavior

Linking kicks off a backfill that walks backwards month-by-month through 16 months of history, then the link flips to live. A nightly job refreshes the trailing 10 days (Google finalizes data with a 2-3 day lag; only finalized data is stored). Syncs are insert-only: every run writes fresh versions and reads collapse to the newest, so retries and overlapping runs never duplicate or hole data. POST …/gsc/sync queues an out-of-band refresh of the recent window.

sync_state is backfill, live, or error (a revoked Google connection lands in error with last_error set; reconnect from settings to resume).

Endpoints

Method Route Result
POST /api/import-oauth/google/start { url } consent URL
GET /api/import-oauth/google/callback OAuth redirect target (public)
GET /api/import-oauth/google/connections List connections (tokens never returned)
DELETE /api/import-oauth/google/connections/{id} Delete connection, revoke at Google, unlink sites
GET /api/sites/{hostname}/gsc/properties List the account's GSC properties
POST /api/sites/{hostname}/gsc/link Link a property, start backfill
DELETE /api/sites/{hostname}/gsc/link Unlink (synced data is kept)
GET /api/sites/{hostname}/gsc/status Link + sync state
POST /api/sites/{hostname}/gsc/sync Queue a refresh (202)
GET /api/sites/{hostname}/gsc/overview Totals, previous period, daily series, tracker overlay
GET /api/sites/{hostname}/gsc/breakdown Top queries / pages / countries / devices

Overview response

GET …/gsc/overview?from=<unix>&to=<unix>&tz=<iana> returns:

json
{
  "totals": { "clicks": 512, "impressions": 39120, "ctr": 0.0131, "position": 18.4 },
  "previous": { "clicks": 480, "impressions": 36600, "ctr": 0.0131, "position": 19.1 },
  "series": [{ "t": 1758326400, "clicks": 31, "impressions": 2404, "ctr": 0.0129, "position": 17.9 }],
  "overlay": [{ "t": 1758326400, "gsc_clicks": 31, "tt_pageviews": 24 }]
}
  • previous covers the equally-sized period directly before the range.
  • position and ctr are impression-weighted, matching the Search Console UI.
  • overlay puts GSC clicks next to the same day's google-referrer pageviews measured by the tracker — a quick read on how many search clicks actually land (ad blockers, bounces before load, and SERP features all eat clicks).
  • Day buckets follow the tz parameter; the underlying GSC days are Pacific Time, so exact boundaries can differ by a few hours from your local tz.

Breakdown response

GET …/gsc/breakdown?dim=queries&from=…&to=…&limit=100 returns { "dim": "queries", "rows": [{ "name", "clicks", "impressions", "ctr", "position" }] } sorted by clicks. countries uses ISO 3166-1 alpha-3 codes (as GSC returns them). limit caps at 500.

GSC anonymizes rare queries and caps rows per day, so query totals can undershoot the daily totals — that's Google's data, not a sync bug.