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.frAuthentication
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_xxxxxxxxxxxxxxxxxxxxxxxxOpenAPI 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.
curl -s https://causalit.fr/api/openapi.json -o causalit-openapi.jsonNothing 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
| Name | Type | Description |
|---|---|---|
drequired | string | Site 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. |
nrequired | string | Event name. Use "pageview" for a page view, otherwise your own name. |
urequired | string (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. |
r | string | Referrer, the document.referrer equivalent. Omit it, or send "", when there is none. |
w | integer | Screen width in pixels. |
sd | integer | Max scroll depth reached, 0-100. |
p | object | Custom 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. |
uid | string | Server-side only. An already-hashed end-user id (identified mode). Never a raw id, never an email. |
ip | string | Server-side only. The end user IP, relayed from your incoming request. Used to derive the daily fingerprint, never stored. |
ua | string | Server-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
| Status | Description |
|---|---|
| 202 | Accepted. 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. |
| 400 | Server-side mode only. Malformed payload: bad JSON, missing or invalid d / n / u, or a body over the size limit. |
| 401 | Server-side mode only. The bearer token is not a valid write-scoped key for an active site. A read-scoped key is rejected here. |
| 402 | Server-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
| Name | Type | Description |
|---|---|---|
periodquery | stringone 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. |
fromquery | string (date) | Inclusive first day, YYYY-MM-DD. Only read when period=custom. |
toquery | string (date) | Inclusive last day, YYYY-MM-DD. Only read when period=custom. |
pathquery | string | Any 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
| Status | Description |
|---|---|
| 200 | Metrics for the range. |
| 401 | Missing, malformed, unknown or revoked API key. |
| 403 | The key is valid but lacks the read scope. A write key can only ingest. |
| 429 | Rate 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
| Name | Type | Description |
|---|---|---|
meta | Meta | Echo of what the server actually measured. The range is half-open: [start, end). |
meta.site | string | Domain of the site the key resolved to. |
meta.period | string | |
meta.start | string (date-time) | Inclusive start, ISO 8601 UTC. |
meta.end | string (date-time) | Exclusive end, ISO 8601 UTC. |
meta.granularity | string | Bucket size actually used: the period default, or what gran asked for when the endpoint accepts it. |
summary | Summary | |
summary.visitors | integer | Distinct daily fingerprints seen in the range. |
summary.pageviews | integer | Pageview events in the range. |
summary.sessions | integer | Visits. Thirty minutes of inactivity closes one. |
summary.bounceRate | number | null | Percentage 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.avgDurationSec | number | null | Mean 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
| Name | Type | Description |
|---|---|---|
periodquery | stringone 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. |
fromquery | string (date) | Inclusive first day, YYYY-MM-DD. Only read when period=custom. |
toquery | string (date) | Inclusive last day, YYYY-MM-DD. Only read when period=custom. |
pathquery | string | Any 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. |
granquery | stringone 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
| Status | Description |
|---|---|
| 200 | One point per bucket, oldest first. |
| 401 | Missing, malformed, unknown or revoked API key. |
| 403 | The key is valid but lacks the read scope. A write key can only ingest. |
| 429 | Rate 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
| Name | Type | Description |
|---|---|---|
meta | Meta | Echo of what the server actually measured. The range is half-open: [start, end). |
meta.site | string | Domain of the site the key resolved to. |
meta.period | string | |
meta.start | string (date-time) | Inclusive start, ISO 8601 UTC. |
meta.end | string (date-time) | Exclusive end, ISO 8601 UTC. |
meta.granularity | string | Bucket size actually used: the period default, or what gran asked for when the endpoint accepts it. |
series[] | SeriesPoint[] | |
series[].bucket | string | Bucket start: YYYY-MM-DD, YYYY-MM-DD HH:00:00 or YYYY-MM, per granularity. |
series[].visitors | integer | |
series[].pageviews | integer |
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
| Name | Type | Description |
|---|---|---|
dimpathrequired | stringone 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. |
periodquery | stringone 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. |
fromquery | string (date) | Inclusive first day, YYYY-MM-DD. Only read when period=custom. |
toquery | string (date) | Inclusive last day, YYYY-MM-DD. Only read when period=custom. |
pathquery | string | Any 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. |
limitquery | integerdefault: 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
| Status | Description |
|---|---|
| 200 | Rows for the dimension, most visitors first. |
| 400 | Unknown dimension. The message lists the valid ones. |
| 401 | Missing, malformed, unknown or revoked API key. |
| 403 | The key is valid but lacks the read scope. A write key can only ingest. |
| 429 | Rate 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
| Name | Type | Description |
|---|---|---|
meta | BreakdownMeta | Echo of what the server actually measured. The range is half-open: [start, end). |
meta.site | string | Domain of the site the key resolved to. |
meta.period | string | |
meta.start | string (date-time) | Inclusive start, ISO 8601 UTC. |
meta.end | string (date-time) | Exclusive end, ISO 8601 UTC. |
meta.granularity | string | Bucket size actually used: the period default, or what gran asked for when the endpoint accepts it. |
meta.dimension | string | The dimension these rows break down. |
rows[] | BreakdownRow[] | |
rows[].key | string | The dimension value. Paths are the grouped route, so /orders/4412 and /orders/4413 share one row. |
rows[].visitors | integer | |
rows[].pageviews | integer |
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
| Name | Type | Description |
|---|---|---|
periodquery | stringone 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. |
fromquery | string (date) | Inclusive first day, YYYY-MM-DD. Only read when period=custom. |
toquery | string (date) | Inclusive last day, YYYY-MM-DD. Only read when period=custom. |
pathquery | string | Any 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
| Status | Description |
|---|---|
| 200 | One row per goal. |
| 401 | Missing, malformed, unknown or revoked API key. |
| 403 | The key is valid but lacks the read scope. A write key can only ingest. |
| 429 | Rate 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
| Name | Type | Description |
|---|---|---|
meta | Meta | Echo of what the server actually measured. The range is half-open: [start, end). |
meta.site | string | Domain of the site the key resolved to. |
meta.period | string | |
meta.start | string (date-time) | Inclusive start, ISO 8601 UTC. |
meta.end | string (date-time) | Exclusive end, ISO 8601 UTC. |
meta.granularity | string | Bucket size actually used: the period default, or what gran asked for when the endpoint accepts it. |
goals[] | Goal[] | |
goals[].id | string (uuid) | |
goals[].name | string | |
goals[].kind | string | Matched on a page path or on a custom event name. |
goals[].pattern | string | The path glob or event name the goal matches. |
goals[].active | boolean | |
goals[].conversions | integer | Conversions in the range. |
goals[].visitors | integer | Distinct visitors who converted at least once. |
goals[].rate | number | Converting visitors as a percentage of all visitors in the range. |
goals[].value | number | Summed 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
| Name | Type | Description |
|---|---|---|
idpathrequired | string (uuid) | Site id (UUID), as it appears in the dashboard URL. |
typequery | stringone of: summary, timeseries, breakdown, goals · default: summary | Which table to export. |
dimquery | stringone 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. |
periodquery | stringone 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. |
fromquery | string (date) | Inclusive first day, YYYY-MM-DD. Only read when period=custom. |
toquery | string (date) | Inclusive last day, YYYY-MM-DD. Only read when period=custom. |
pathquery | string | Any 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. |
limitquery | integerdefault: 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.csvResponses
| Status | Description |
|---|---|
| 200 | A text/csv attachment. |
| 400 | Unknown breakdown dimension. |
| 403 | The key is valid but belongs to another site. |
| 404 | No such site, or no access to it. |
{
"error": "This key is not authorized for this site."
}