Skip to content

Query API endpoints

The query API lives at api.leadmaps.nl. All endpoints accept and return JSON. This page documents every header, status code, and error envelope you will deal with as a caller.

Two modes, both via Authorization: Bearer <...>:

  • admin - an admin token for cross-workspace operator access.
  • apiKey - a workspace API key, with one of the scopes: ingest:write, query:read, admin.

A non-admin caller probing a workspace it has no access to gets a 404 with the same envelope as “not found”. leadmaps never confirms the existence of a workspace you cannot see through a status-code difference.

MethodPathAuth
GET/healthznone
GET/workspacesadmin
POST/workspacesadmin
GET/workspaces/:slugadmin
PATCH/workspaces/:slugadmin
GET/workspaces/:slug/sitesadmin
POST/workspaces/:slug/sitesadmin
DELETE/workspaces/:slug/sites/:idadmin
GET/workspaces/:slug/keysadmin or admin-scoped api_key
POST/workspaces/:slug/keysadmin or admin-scoped api_key
DELETE/workspaces/:slug/keys/:keyIdadmin or admin-scoped api_key
GET/workspaces/:slug/auditadmin or admin-scoped api_key
GET/sites/:siteId/eventsadmin or query:read api_key
GET/sites/:siteId/pageviewsadmin or query:read api_key
GET/sites/:siteId/sessionsadmin or query:read api_key
GET/sites/:siteId/reports/summary-v2admin or query:read api_key
GET/sites/:siteId/reports/pages-v2admin or query:read api_key
GET/sites/:siteId/reports/sources-v2admin or query:read api_key
GET/sites/:siteId/reports/campaigns-v2admin or query:read api_key
GET/sites/:siteId/events/list-v2admin or query:read api_key
GET/sites/:siteId/events/overview-v2admin or query:read api_key
GET/sites/:siteId/events/activity-v2admin or query:read api_key
GET/sites/:siteId/events/stream-v2admin or query:read api_key
GET/sites/:siteId/users/:userId/timelineadmin or query:read api_key
POST/workspaces/:slug/analysis/funnels-v2admin or query:read api_key
POST/workspaces/:slug/analysis/funnels-v2/correlationadmin or query:read api_key
POST/workspaces/:slug/analysis/funnels-v2/explanationadmin or query:read api_key
GET/workspaces/:slug/attribution/resultsadmin or query:read api_key
GET/workspaces/:slug/roiadmin or query:read api_key
GET/workspaces/:slug/roi/compareadmin or query:read api_key
GET/workspaces/:slug/spendadmin or query:read api_key
POST/workspaces/:slug/spendadmin or admin-scoped api_key
POST/workspaces/:slug/spend/csvadmin or admin-scoped api_key
PATCH/workspaces/:slug/spend/:idadmin or admin-scoped api_key
DELETE/workspaces/:slug/spendadmin or admin-scoped api_key
GET/v2/sites/:siteId/consent-banner/configadmin or query:read api_key
PUT/v2/sites/:siteId/consent-banner/configadmin or admin-scoped api_key
POST/sites/:siteId/sourcemapssite bearer
GET/sites/:siteId/dlqadmin or admin-scoped api_key
POST/sites/:siteId/dlq/:eventId/replayadmin or admin-scoped api_key
POST/sites/:siteId/gdpr/exportadmin or admin-scoped api_key
POST/sites/:siteId/gdpr/deleteadmin or admin-scoped api_key
POST/webhooks/:adapter/:siteIdper-adapter signature

All analytics endpoints sit under /sites/:siteId/. A request for a site that does not exist or that you cannot access returns 404. from and to are YYYY-MM-DD and inclusive on both ends.

The dashboard uses the V2 report and Events endpoints listed above. Every JSON report has a strict { meta, data } envelope. meta records the exact scope, measure definitions, freshness watermark, completeness, warnings, truncation, and cursor state applied by the server. A successful response that does not match the shared runtime schema is rejected by the dashboard instead of being rendered as plausible data.

Common report filters are:

  • from and to, required UTC dates. Exact report and historical Events ranges support at most 367 inclusive days and reject a start date after the end date.
  • scope=all-sites for a workspace-wide result. Without it, :siteId is the population.
  • source for one acquisition or referrer source.
  • device, browser, os, country, region, city, and path. Each is a comma-separated list with at most 20 values.
  • internal=include to include traffic tagged as internal. The default is exclude.
  • timezone=UTC and bots=exclude are the only accepted values. Customer analytics cannot opt bot traffic back in.

pages-v2 accepts include_params, limit from 1 to 200, and an opaque cursor. sources-v2 and campaigns-v2 accept limit from 1 to 200. The response metadata tells you whether rows were truncated or another cursor page exists.

Events V2 separates three read models:

  • list-v2 is a date-bounded, ingest-ordered event list with limit from 1 to 200 and an opaque cursor.
  • overview-v2 returns exact event totals and the top event types for a date range.
  • activity-v2 returns one dense 60-minute window and twelve five-minute buckets per top path.
  • stream-v2 is an SSE tail. It accepts an opaque resume cursor plus optional name and anon_id filters. The initial cursor frame is valid even when no new events are waiting.

Funnel V2 accepts one complete query object with 2 to 12 ordered steps, a completion window, date mode, site scope, source and dimension filters, internal-traffic choice, and an optional breakdown. The correlation and explanation endpoints take that same query plus the selected transition and execution identity, so auxiliary panels cannot analyze a different population.

Attribution results require model, from, and to ISO dates or timezone-qualified timestamps. The inclusive range can span at most 366 days. They accept conversion_id, an opaque cursor, and limit from 1 to 10,000. ROI and spend reads use inclusive YYYY-MM-DD ranges of at most 366 days. ROI stays workspace-wide, keeps totals separated by currency, and returns ratio fields only when spend and revenue have one compatible currency.

{
"count": 12345,
"by_day": [{ "date": "2026-04-01", "count": 412 }]
}

400 invalid_date_param when missing/malformed; 400 invalid_date_range when from > to.

{
"by_name": [{ "name": "pageview", "count": 12345 }]
}

Sorted by count desc, then name asc.

{
"count": 1024,
"avg_duration_s": 187.4,
"avg_events": 4.2,
"bounce_rate": 0.314
}

Empty range yields all zeros (never null or NaN).

GET /sites/:siteId/users/:userId/timeline?limit=&before=

Section titled “GET /sites/:siteId/users/:userId/timeline?limit=&before=”

Per-user event timeline. Resolves cross-device merges so a row appears for every anonymous id ever bound to the user.

{
"events": [
{
"id": "<uuid>",
"type": "<event-type>",
"url": "" | null,
"ts": "<iso>",
"anon_id": "",
"referrer": "" | null,
"country": "NL" | null,
"city": "Amsterdam" | null,
"browser": "Chrome" | null,
"os": "macOS" | null,
"device_type": "desktop" | "mobile" | "tablet" | "other" | null
}
],
"next_before": "<iso>" | null
}

404 user_not_found when no known user matches (site_id, user_id).

The raw event payload and the parsed user-agent string are deliberately omitted from this response, because they may carry PII you never intended to expose at a per-user surface.

The authenticated control-plane endpoints at /v2/sites/:siteId/consent-banner/config read and save the safe banner model used by the dashboard. A save includes the last config_version, a unique idempotency_key, and an explicit request_reconsent choice. Stale saves return 409 so one browser cannot silently overwrite another browser’s work. Requests made with the platform administrator credential also include X-Workspace-Id; the API compares it with the site’s resolved workspace. Workspace API keys are already pinned to their own workspace by authentication.

The endpoint accepts only the bounded content and design controls described in Customize the consent banner. It rejects raw HTML, CSS, scripts, and unknown fields. This is not a browser bootstrap endpoint: generated installation code embeds the validated settings so a visitor’s browser does not contact leadmaps to fetch a banner before consent.

Sourcemap upload. Authenticate with the site bearer for that site. A request for a site you cannot access returns 401, not 403, so a probing caller cannot confirm a site exists.

// request
{ "release": "<git-sha>", "file": "<basename.js>", "map": "<base64>" }
// 201
{ "size": 12345 }

Body cap: 25 MB decoded. Idempotent upsert on (site_id, release, file).

Cursor-paginated DLQ rows.

{
"events": [
{
"id": "<uuid>",
"site_id": "site_marketing",
"payload": { /* original wire JSON */ },
"error": [{ "path": "/total_cents", "message": "must be >= 0" }],
"received_at": "<iso>"
}
],
"next_cursor": "<iso>" | null
}

Re-submits the row’s payload to the ingest endpoint.

  • 200 { id, replayed_at } on accept. The DLQ row is deleted.
  • 502 ingest_rejected on any other status. The DLQ row stays in place.
  • 502 ingest_unreachable on network error.

The replay re-enters the full ingest pipeline by design, so consent, rate-limit, and dedup gates are re-evaluated.

See GDPR endpoints for the full request / response walkthrough. Both export and delete are admin or admin-scoped api_key.

See Audit log. Cursor-paginated read at GET /workspaces/:slug/audit.

See Workspaces + API keys for the lifecycle walkthrough. The CRUD endpoints follow REST conventions. Check the error compatibility note below when one client calls both site-level V2 routes and older workspace routes.

See Webhooks.

Site-level V2 report and Events endpoints use the structured envelope:

{
"error": {
"code": "<stable_string>",
"message": "<human-readable>",
"details": [/* optional */]
}
}

code is part of the V2 contract. message may evolve. details carries structured per-field errors, such as schema validation failures.

Some older workspace endpoints, including attribution, ROI, and spend, retain their compatibility envelope:

{
"error": "<stable_string>",
"message": "<human-readable>"
}

Treat the nested error.code and the flat error value as the stable machine codes for their respective endpoint families. Do not parse human-readable messages. A future versioned migration can make the shapes uniform without silently breaking existing clients.