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/users/:userId/timelineadmin or query:read 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.

{
"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.

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 and the same error envelope as everything else.

See Webhooks.

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

code is part of the contract. message may evolve. details carries structured per-field errors (e.g. schema validation failures).