API reference

Send events from your servers, pull your aggregates back out, and see what the endpoint actually answered. Every example on this page is addressed to this instance.

Base URL

Every path below is relative to this origin. It is the only host that answers — there is no separate API subdomain, so a request sent anywhere else never reaches us at all.

https://causalit.fr

Authentication

One credential type: a site API key, created in Site → Settings → API keys and sent as a bearer token. The key resolves the site server-side, so no endpoint takes a site id.

  • A write key sends events. It is the only key the ingestion endpoint accepts.
  • A read key pulls statistics. It is the only key the read endpoints accept.
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxx

OpenAPI document

The same contract as a machine-readable OpenAPI 3.1 file. Point Swagger UI, Postman, Insomnia or a client generator at it — its servers entry already carries this deployment’s base URL.

Download openapi.json

curl -s https://causalit.fr/api/openapi.json -o causalit-openapi.json

Nothing arrives?

Ingestion is silent from a browser by design, and explicit with a key. Work down this list — the first two account for almost every case.

  • Check the host. Events must go to https://causalit.fr/api/ev. A request to any other hostname resolves elsewhere, or nowhere, and never reaches your account.
  • Check the scope. Ingestion needs a write key; a read key is rejected with 401, exactly like an unknown one.
  • Check the URL field. u must be a full absolute URL, scheme included. A bare path is a 400.
  • Read the status code, and log it. With a bearer token the endpoint answers honestly: 202 accepted, 400 malformed payload, 401 key problem, 402 monthly quota spent. Use curl -i while you integrate.
  • Look at "Last used" on the key. Still empty means no authenticated request ever arrived, so the problem is the host, the header or the network — not the payload.

Sending events

POST/api/ev

Send an eventNo key, or a write key

The one endpoint every collector writes to: the browser tag and your own server code both POST this shape.

Two modes share the URL. With no Authorization header it is browser mode: the site is resolved from d, and the visitor IP and user-agent are read from the request itself. With Authorization: Bearer sk_... (a write-scoped key) it is server-side mode: the site is resolved from the key, and you relay the end user ip and ua yourself. Omit those two and every server-side event is attributed to your server — one visitor, all day.

Send the body as Content-Type: text/plain. That is deliberate: it keeps the browser request a CORS simple request, with no preflight round-trip. The server reads the body as text and parses JSON either way, so application/json also works from a server.

Identity is always derived server-side. No field lets a caller claim a visitor id, a session or a collector.

Request body

A JSON object, serialized as text. Content-Type: text/plain

NameTypeDescription
drequiredstringSite domain exactly as registered here: bare host, no scheme, no path. Ignored in server-side mode, where the key decides the site — but still required by the parser, so always send it.
nrequiredstringEvent name. Use "pageview" for a page view, otherwise your own name.
urequiredstring (uri)Full absolute URL of the page. Its path is stored and its UTM parameters extracted. A relative URL is rejected — this is the most common cause of a 400.
rstringReferrer, the document.referrer equivalent. Omit it, or send "", when there is none.
wintegerScreen width in pixels.
sdintegerMax scroll depth reached, 0-100.
pobjectCustom properties: flat, at most 30 keys, keys under 100 chars, values truncated at 300. Numbers and booleans are coerced to strings; nested objects and arrays are dropped. Never put personal data here.
uidstringServer-side only. An already-hashed end-user id (identified mode). Never a raw id, never an email.
ipstringServer-side only. The end user IP, relayed from your incoming request. Used to derive the daily fingerprint, never stored.
uastringServer-side only. The end user user-agent, relayed from your incoming request.

Example

curl -i -X POST "https://causalit.fr/api/ev"   -H "Authorization: Bearer $CAUSALIT_WRITE_KEY"   -H "Content-Type: text/plain"   --data '{"d":"example.com","n":"signup","u":"https://example.com/signup","p":{"plan":"pro"},"ip":"203.0.113.7","ua":"Mozilla/5.0 (...)"}'

Responses

StatusDescription
202Accepted. In browser mode every outcome answers 202 with an empty body — including events that were dropped (bot, unknown or inactive site, rate limit) — so ingestion logic never leaks to the page.
400Server-side mode only. Malformed payload: bad JSON, missing or invalid d / n / u, or a body over the size limit.
401Server-side mode only. The bearer token is not a valid write-scoped key for an active site. A read-scoped key is rejected here.
402Server-side mode only. The organization is past its monthly event quota; the event was counted as rejected, not stored.

Reading statistics

GET/api/v1/summary

Headline metricsRead key

The five numbers at the top of the dashboard, for the selected range and filters.

Parameters

NameTypeDescription
periodquerystring
one of: day, yesterday, 7d, 14d, 30d, 90d, 6m, 12m, mtd, last_month, ytd, custom · default: 7d
Time window. "day" is today; "custom" reads from and to. Ranges are computed in UTC.
fromquerystring (date)Inclusive first day, YYYY-MM-DD. Only read when period=custom.
toquerystring (date)Inclusive last day, YYYY-MM-DD. Only read when period=custom.
pathquerystringAny dimension passed as a query parameter narrows the result to that value — path=/pricing, country=FR, source=google. Filterable dimensions: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, event. Path filters match the grouped route.

Example

curl -s "https://causalit.fr/api/v1/summary?period=30d"   -H "Authorization: Bearer $CAUSALIT_READ_KEY"

Responses

StatusDescription
200Metrics for the range.
401Missing, malformed, unknown or revoked API key.
403The key is valid but lacks the read scope. A write key can only ingest.
429Rate limit exceeded — about 5 requests/second per site, bursts of 60.
{
  "meta": {
    "site": "example.com",
    "period": "7d",
    "start": "2026-08-17T00:00:00.000Z",
    "end": "2026-08-24T00:00:00.000Z",
    "granularity": "day"
  },
  "summary": {
    "visitors": 1234,
    "pageviews": 4567,
    "sessions": 1500,
    "bounceRate": 42,
    "avgDurationSec": 95
  }
}
Response fields
NameTypeDescription
metaMetaEcho of what the server actually measured. The range is half-open: [start, end).
meta.sitestringDomain of the site the key resolved to.
meta.periodstring
meta.startstring (date-time)Inclusive start, ISO 8601 UTC.
meta.endstring (date-time)Exclusive end, ISO 8601 UTC.
meta.granularitystringBucket size actually used: the period default, or what gran asked for when the endpoint accepts it.
summarySummary
summary.visitorsintegerDistinct daily fingerprints seen in the range.
summary.pageviewsintegerPageview events in the range.
summary.sessionsintegerVisits. Thirty minutes of inactivity closes one.
summary.bounceRatenumber | nullPercentage of sessions with a single pageview and no event. `null` when the range holds no session — there is nothing to judge, and a 0 would read as "nobody bounced".
summary.avgDurationSecnumber | nullMean session duration, in seconds. `null` when the range holds no session.

GET/api/v1/timeseries

Visitors and pageviews over timeRead key

One row per bucket. The bucket size follows the period unless gran overrides it, and is always stated in meta.granularity: hourly for day, monthly for a full year, daily otherwise. Empty buckets come back as zeros rather than being skipped.

Parameters

NameTypeDescription
periodquerystring
one of: day, yesterday, 7d, 14d, 30d, 90d, 6m, 12m, mtd, last_month, ytd, custom · default: 7d
Time window. "day" is today; "custom" reads from and to. Ranges are computed in UTC.
fromquerystring (date)Inclusive first day, YYYY-MM-DD. Only read when period=custom.
toquerystring (date)Inclusive last day, YYYY-MM-DD. Only read when period=custom.
pathquerystringAny dimension passed as a query parameter narrows the result to that value — path=/pricing, country=FR, source=google. Filterable dimensions: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, event. Path filters match the grouped route.
granquerystring
one of: hour, day, week, month
Bucket size. Optional: each period has a default (hourly for day, monthly for a year, daily otherwise) and this overrides it. A size the range cannot carry — hourly over a year, monthly over a week — is ignored rather than rejected, and meta.granularity always states what was actually used.

Example

curl -s "https://causalit.fr/api/v1/timeseries?period=7d"   -H "Authorization: Bearer $CAUSALIT_READ_KEY"

Responses

StatusDescription
200One point per bucket, oldest first.
401Missing, malformed, unknown or revoked API key.
403The key is valid but lacks the read scope. A write key can only ingest.
429Rate limit exceeded — about 5 requests/second per site, bursts of 60.
{
  "meta": {
    "site": "example.com",
    "period": "7d",
    "start": "2026-08-17T00:00:00.000Z",
    "end": "2026-08-24T00:00:00.000Z",
    "granularity": "day"
  },
  "series": [
    {
      "bucket": "2026-08-17",
      "visitors": 120,
      "pageviews": 340
    },
    {
      "bucket": "2026-08-18",
      "visitors": 143,
      "pageviews": 402
    }
  ]
}
Response fields
NameTypeDescription
metaMetaEcho of what the server actually measured. The range is half-open: [start, end).
meta.sitestringDomain of the site the key resolved to.
meta.periodstring
meta.startstring (date-time)Inclusive start, ISO 8601 UTC.
meta.endstring (date-time)Exclusive end, ISO 8601 UTC.
meta.granularitystringBucket size actually used: the period default, or what gran asked for when the endpoint accepts it.
series[]SeriesPoint[]
series[].bucketstringBucket start: YYYY-MM-DD, YYYY-MM-DD HH:00:00 or YYYY-MM, per granularity.
series[].visitorsinteger
series[].pageviewsinteger

GET/api/v1/breakdown/{dim}

Top values for one dimensionRead key

The dashboard tables — top pages, sources, countries, devices — as JSON, ordered by visitors.

Paths come back as grouped routes, not raw URLs: /orders/4412 and /orders/4413 are one row, spelled the way this site path grouping is configured.

Every dimension the dashboard tables cover is available here: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer.

Parameters

NameTypeDescription
dimpathrequiredstring
one of: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer
Which dimension to break down.
periodquerystring
one of: day, yesterday, 7d, 14d, 30d, 90d, 6m, 12m, mtd, last_month, ytd, custom · default: 7d
Time window. "day" is today; "custom" reads from and to. Ranges are computed in UTC.
fromquerystring (date)Inclusive first day, YYYY-MM-DD. Only read when period=custom.
toquerystring (date)Inclusive last day, YYYY-MM-DD. Only read when period=custom.
pathquerystringAny dimension passed as a query parameter narrows the result to that value — path=/pricing, country=FR, source=google. Filterable dimensions: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, event. Path filters match the grouped route.
limitqueryinteger
default: 20 · 1–1000
How many rows to return, most visitors first.

Example

curl -s "https://causalit.fr/api/v1/breakdown/source?period=30d&limit=10"   -H "Authorization: Bearer $CAUSALIT_READ_KEY"

Responses

StatusDescription
200Rows for the dimension, most visitors first.
400Unknown dimension. The message lists the valid ones.
401Missing, malformed, unknown or revoked API key.
403The key is valid but lacks the read scope. A write key can only ingest.
429Rate limit exceeded — about 5 requests/second per site, bursts of 60.
{
  "meta": {
    "site": "example.com",
    "period": "7d",
    "start": "2026-08-17T00:00:00.000Z",
    "end": "2026-08-24T00:00:00.000Z",
    "granularity": "day",
    "dimension": "source"
  },
  "rows": [
    {
      "key": "google",
      "visitors": 300,
      "pageviews": 480
    },
    {
      "key": "direct",
      "visitors": 210,
      "pageviews": 265
    }
  ]
}
Response fields
NameTypeDescription
metaBreakdownMetaEcho of what the server actually measured. The range is half-open: [start, end).
meta.sitestringDomain of the site the key resolved to.
meta.periodstring
meta.startstring (date-time)Inclusive start, ISO 8601 UTC.
meta.endstring (date-time)Exclusive end, ISO 8601 UTC.
meta.granularitystringBucket size actually used: the period default, or what gran asked for when the endpoint accepts it.
meta.dimensionstringThe dimension these rows break down.
rows[]BreakdownRow[]
rows[].keystringThe dimension value. Paths are the grouped route, so /orders/4412 and /orders/4413 share one row.
rows[].visitorsinteger
rows[].pageviewsinteger

GET/api/v1/goals

Conversions per goalRead key

Every goal defined on the site, active or not, with its conversions for the range. A goal that never fired is returned with zeros — the list is the site configuration, not only what happened.

Parameters

NameTypeDescription
periodquerystring
one of: day, yesterday, 7d, 14d, 30d, 90d, 6m, 12m, mtd, last_month, ytd, custom · default: 7d
Time window. "day" is today; "custom" reads from and to. Ranges are computed in UTC.
fromquerystring (date)Inclusive first day, YYYY-MM-DD. Only read when period=custom.
toquerystring (date)Inclusive last day, YYYY-MM-DD. Only read when period=custom.
pathquerystringAny dimension passed as a query parameter narrows the result to that value — path=/pricing, country=FR, source=google. Filterable dimensions: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, event. Path filters match the grouped route.

Example

curl -s "https://causalit.fr/api/v1/goals?period=30d"   -H "Authorization: Bearer $CAUSALIT_READ_KEY"

Responses

StatusDescription
200One row per goal.
401Missing, malformed, unknown or revoked API key.
403The key is valid but lacks the read scope. A write key can only ingest.
429Rate limit exceeded — about 5 requests/second per site, bursts of 60.
{
  "meta": {
    "site": "example.com",
    "period": "7d",
    "start": "2026-08-17T00:00:00.000Z",
    "end": "2026-08-24T00:00:00.000Z",
    "granularity": "day"
  },
  "goals": [
    {
      "id": "0f2c1a7e-...",
      "name": "Signup",
      "kind": "path",
      "pattern": "/welcome",
      "active": true,
      "conversions": 42,
      "visitors": 40,
      "rate": 3.5,
      "value": 0
    }
  ]
}
Response fields
NameTypeDescription
metaMetaEcho of what the server actually measured. The range is half-open: [start, end).
meta.sitestringDomain of the site the key resolved to.
meta.periodstring
meta.startstring (date-time)Inclusive start, ISO 8601 UTC.
meta.endstring (date-time)Exclusive end, ISO 8601 UTC.
meta.granularitystringBucket size actually used: the period default, or what gran asked for when the endpoint accepts it.
goals[]Goal[]
goals[].idstring (uuid)
goals[].namestring
goals[].kindstringMatched on a page path or on a custom event name.
goals[].patternstringThe path glob or event name the goal matches.
goals[].activeboolean
goals[].conversionsintegerConversions in the range.
goals[].visitorsintegerDistinct visitors who converted at least once.
goals[].ratenumberConverting visitors as a percentage of all visitors in the range.
goals[].valuenumberSummed monetary value, when the goal carries one.

CSV export

GET/api/sites/{id}/export

Download stats as CSVRead key or session

The same aggregates as the read API, as a CSV attachment (UTF-8 with a BOM, so spreadsheets read accents correctly).

The only endpoint that takes a site id in the URL, because the dashboard export buttons call it with your session. With a key, the key must belong to that site.

Parameters

NameTypeDescription
idpathrequiredstring (uuid)Site id (UUID), as it appears in the dashboard URL.
typequerystring
one of: summary, timeseries, breakdown, goals · default: summary
Which table to export.
dimquerystring
one of: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer
Required when type=breakdown. Same values as the breakdown endpoint.
periodquerystring
one of: day, yesterday, 7d, 14d, 30d, 90d, 6m, 12m, mtd, last_month, ytd, custom · default: 7d
Time window. "day" is today; "custom" reads from and to. Ranges are computed in UTC.
fromquerystring (date)Inclusive first day, YYYY-MM-DD. Only read when period=custom.
toquerystring (date)Inclusive last day, YYYY-MM-DD. Only read when period=custom.
pathquerystringAny dimension passed as a query parameter narrows the result to that value — path=/pricing, country=FR, source=google. Filterable dimensions: path, source, country, region, city, browser, os, device, utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer, event. Path filters match the grouped route.
limitqueryinteger
default: 20 · 1–1000
How many rows to export. Defaults to 1000 here, not 20.

Example

curl -sL "https://causalit.fr/api/sites/SITE_ID/export?type=breakdown&dim=source&period=30d"   -H "Authorization: Bearer $CAUSALIT_READ_KEY" -o sources.csv

Responses

StatusDescription
200A text/csv attachment.
400Unknown breakdown dimension.
403The key is valid but belongs to another site.
404No such site, or no access to it.
{
  "error": "This key is not authorized for this site."
}

Start free

API reference — causalIT