{"openapi":"3.1.0","info":{"title":"Aback Analytics API","version":"1.0.0","summary":"Website analytics, event ingestion and revenue attribution over HTTP.","description":"The Aback Analytics API covers three things: sending events from a browser, sending events and confirmed payments from a server, and reading the reports of one website.\n\nThe public tracking id of a website is `abk_` followed by 16 characters. It is public and belongs in the browser snippet.\n\nAn API key is `abk_xxxxxxxx.secret`. The part before the dot is the public key prefix, the part after the dot is the secret. The secret is shown exactly once, is stored only as a keyed digest, and belongs on your server only. A key identifies exactly one website, so a website id in a request body is checked against the key and a mismatch is refused.\n\nEvery response uses one envelope. A successful call answers `{ \"ok\": true, \"data\": ... }`. A failed call answers `{ \"ok\": false, \"error\": { \"code\": ..., \"message\": ... }, \"requestId\": ... }`. The only exception is the CSV export, which answers with the file itself.\n\nEvery response carries an `x-request-id` header. Quote the request id when you report a problem.","license":{"name":"Proprietary","url":"https://abackanalytics.com/terms"}},"servers":[{"url":"https://abackanalytics.com","description":"Production"},{"url":"http://localhost:3001","description":"Local development"}],"tags":[{"name":"Ingestion","description":"Sending events and confirmed payments into the product."},{"name":"Reports","description":"Reading the reports of one website."},{"name":"Account","description":"Reading the websites of the signed in account."},{"name":"Auth","description":"Creating a session."},{"name":"Meta","description":"The specification itself."}],"security":[{"ApiKeyAuth":[]}],"paths":{"/api/collect":{"post":{"tags":["Ingestion"],"summary":"Record browser events","description":"The endpoint the browser tracking script posts to. It accepts a batch of pageviews and custom events for one website and answers with the number of events that were accepted and the number that were rejected.\n\nNo credential is needed. The website is identified by the public tracking id in the body, and the request origin is checked against the canonical domain and the verified domains of the website when the origin policy is set to verify.\n\nThe response carries the cross-origin header when the origin is allowed, so a browser on the customer's site can read it. A refused origin gets no such header.\n\n```bash\ncurl -X POST https://abackanalytics.com/api/collect \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"websiteId\": \"abk_xxxxxxxxxxxxxxxx\",\n    \"events\": [\n      {\n        \"id\": \"e6f1c0a2b3c4d5e6f7081\",\n        \"type\": \"pageview\",\n        \"url\": \"https://example.com/pricing?utm_source=twitter\",\n        \"title\": \"Pricing\",\n        \"referrer\": \"https://t.co/abcd\",\n        \"visitorId\": \"v_9f8e7d6c5b4a3210ffee\",\n        \"timestamp\": 1790000000000\n        \"screenWidth\": 1440\n        \"language\": \"en\"\n      }\n      {\n        \"id\": \"a1b2c3d4e5f60718293a\",\n        \"type\": \"event\",\n        \"name\": \"signup_completed\",\n        \"properties\": { \"plan\": \"pro\" },\n        \"visitorId\": \"v_9f8e7d6c5b4a3210ffee\"\n      }\n    ]\n  }'\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": { \"accepted\": 2, \"rejected\": 0 }\n}\n```\n\nA website that does not exist or is not active is answered with `accepted: 0` and the events counted as rejected. The endpoint never reveals whether a tracking id exists.","security":[],"x-curl":"curl -X POST https://abackanalytics.com/api/collect -H \"content-type: application/json\" -d '{\"websiteId\":\"abk_xxxxxxxxxxxxxxxx\",\"events\":[{\"id\":\"e6f1c0a2b3c4d5e6f7081\",\"type\":\"pageview\",\"url\":\"https://example.com/pricing\",\"title\":\"Pricing\",\"visitorId\":\"v_9f8e7d6c5b4a3210ffee\"}]}'","x-rate-limit":{"limit":120,"periodSeconds":60,"key":"website id and client address","endpoints":["POST /api/collect"]},"requestBody":{"required":true,"description":"A batch of at most 20 browser events. The body is at most 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectRequest"},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","events":[{"id":"e6f1c0a2b3c4d5e6f7081","type":"pageview","url":"https://example.com/pricing?utm_source=twitter","title":"Pricing","referrer":"https://t.co/abcd","visitorId":"v_9f8e7d6c5b4a3210ffee","timestamp":1790000000000,"screenWidth":1440,"language":"en"}]}}}},"responses":{"200":{"description":"The batch was processed. The counts say how many events were accepted and how many were rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CollectResult"},"example":{"accepted":1,"rejected":0}}}},"400":{"description":"The body is not valid JSON, or the batch does not match the schema. The details list the field and the reason for each issue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"The monthly event limit of the plan has been reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The origin is not one of the verified domains of the website.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"The body is missing, or it is larger than 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many requests for this website.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/events":{"post":{"tags":["Ingestion"],"summary":"Record one event from a server","description":"Sends one event from your own backend, for example a signup that completes in a webhook or a job that finishes.\n\nThe key needs the `events:write` scope. The server generates the event id, so the endpoint has no client idempotency key. If your backend retries a request, send a new event id by sending a new request, and treat each answer as one event.\n\nA server event counts against the monthly event quota like a browser event. The endpoint is rate limited per API key, 120 requests a minute.\n\n```bash\ncurl -X POST https://abackanalytics.com/api/v1/events \\\n  -H \"Authorization: Bearer abk_xxxxxxxx.secret\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"websiteId\": \"abk_xxxxxxxxxxxxxxxx\",\n    \"event\": \"signup_completed\",\n    \"properties\": { \"plan\": \"pro\" },\n    \"timestamp\": 1790000000000,\n    \"visitorId\": \"v_9f8e7d6c5b4a3210ffee\",\n    \"sessionId\": \"s_1122334455\",\n    \"path\": \"/signup\"\n  }'\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": { \"eventId\": \"evt_9f2c41b7ac0d4e6f8a1b\", \"status\": \"accepted\" }\n}\n```","security":[{"ApiKeyAuth":[]}],"x-curl":"curl -X POST https://abackanalytics.com/api/v1/events -H \"Authorization: Bearer abk_xxxxxxxx.secret\" -H \"content-type: application/json\" -d '{\"websiteId\":\"abk_xxxxxxxxxxxxxxxx\",\"event\":\"signup_completed\",\"properties\":{\"plan\":\"pro\"}}'","x-required-scope":"events:write","x-idempotency":"The event id is generated by the server, so a retry records a second event. There is no client idempotency key on this endpoint in this version.","requestBody":{"required":true,"description":"The event to record. The body is at most 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerEventRequest"},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","event":"signup_completed","properties":{"plan":"pro"},"timestamp":1790000000000,"visitorId":"v_9f8e7d6c5b4a3210ffee","sessionId":"s_1122334455","path":"/signup"}}}},"responses":{"200":{"description":"The event was already recorded. The event id and session id of the stored event are returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerEventResult"},"example":{"eventId":"evt_9f2c41b7ac0d4e6f8a1b","status":"duplicate"}}}},"202":{"description":"The event was accepted and is queued for processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerEventResult"},"example":{"eventId":"evt_9f2c41b7ac0d4e6f8a1b","status":"accepted"}}}},"400":{"description":"The body is not valid JSON, or it does not match the schema. The details list the field and the reason for each issue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Server side events are not available on the current plan (plan_restricted), or the monthly event quota is spent (quota_exceeded).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the events:write scope, the website does not belong to the key, or the site is not accepting events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"The body is larger than 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"More than 120 requests in a minute with this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/revenue":{"post":{"tags":["Ingestion"],"summary":"Record one confirmed payment","description":"Records a payment that your own system has already confirmed. The browser never sends an amount, and this endpoint is the authoritative source for revenue.\n\nThe key needs the `revenue:write` scope. The amount is an integer in minor units, so 1999 means 19.99. The currency is a three letter ISO code and currencies are never mixed in a report.\n\nThe idempotency key is what makes the call safe to retry. Repeating the same key never double counts revenue. The second call answers 200 with the status duplicate.\n\n```bash\ncurl -X POST https://abackanalytics.com/api/v1/revenue \\\n  -H \"Authorization: Bearer abk_xxxxxxxx.secret\" \\\n  -H \"content-type: application/json\" \\\n  -d '{\n    \"websiteId\": \"abk_xxxxxxxxxxxxxxxx\",\n    \"idempotencyKey\": \"order_2026_10_00041\",\n    \"amount\": 1999,\n    \"currency\": \"USD\",\n    \"type\": \"payment\",\n    \"customerRef\": \"cust_8f21\",\n    \"productId\": \"pro-monthly\"\n  }'\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"revenueEventId\": \"rev_3c9d5a1e7b2f4a6c8d0e\",\n    \"status\": \"recorded\",\n    \"attributed\": true\n  }\n}\n```\n\nThe attributed flag says whether a touchpoint was found for the visitor inside the attribution window of the website. It is false when the payment arrives without a visitor id, and when the visitor is not known to the site.","security":[{"ApiKeyAuth":[]}],"x-curl":"curl -X POST https://abackanalytics.com/api/v1/revenue -H \"Authorization: Bearer abk_xxxxxxxx.secret\" -H \"content-type: application/json\" -d '{\"websiteId\":\"abk_xxxxxxxxxxxxxxxx\",\"idempotencyKey\":\"order_2026_10_00041\",\"amount\":1999,\"currency\":\"USD\",\"customerRef\":\"cust_8f21\"}'","x-required-scope":"revenue:write","x-idempotency":"The idempotency key is stored with the payment. Repeating it answers 200 with the status duplicate and does not change any number in a report.","requestBody":{"required":true,"description":"The confirmed payment. The body is at most 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevenueEventRequest"},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","idempotencyKey":"order_2026_10_00041","amount":1999,"currency":"USD","type":"payment","customerRef":"cust_8f21","productId":"pro-monthly","visitorId":"v_9f8e7d6c5b4a3210ffee","sessionId":"s_1122334455","timestamp":1790000000000}}}},"responses":{"200":{"description":"The idempotency key was already used. Nothing was counted twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevenueEventResult"},"example":{"revenueEventId":"rev_3c9d5a1e7b2f4a6c8d0e","status":"duplicate","attributed":true}}}},"201":{"description":"The payment was recorded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevenueEventResult"},"example":{"revenueEventId":"rev_3c9d5a1e7b2f4a6c8d0e","status":"recorded","attributed":true}}}},"400":{"description":"The body is not valid JSON, or it does not match the schema, or the timestamp is before 2000-01-01 or more than a day in the future. The details list the field and the reason for each issue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Revenue tracking is not available on the current plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the revenue:write scope, or the website does not belong to the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"The body is larger than 32768 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/identify":{"post":{"tags":["Ingestion"],"summary":"Link a visitor to one of your users","description":"Links the browser that `aback.identifier()` names to your own user id, from your backend. A link made here needs the secret key, and when a user has one, attribution uses the links made here and not the ones the browser sent. Each browser stays one visitor in every count. An email address is refused as a user id.","security":[{"ApiKeyAuth":[]}],"x-required-scope":"events:write","x-idempotency":"Linking the same visitor and user again only moves the last seen time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["websiteId","visitorId","userId"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64},"visitorId":{"type":"string","minLength":16,"maxLength":64,"description":"The value of aback.identifier() in the visitor's browser."},"userId":{"type":"string","minLength":1,"maxLength":128}}},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","visitorId":"4d1c2b7a-0f3e-4a51-9c88-1b6f0a2d5e73","userId":"usr_8f3k2"}}}},"responses":{"200":{"description":"The visitor is linked to the user.","content":{"application/json":{"schema":{"type":"object","properties":{"linked":{"type":"boolean"}}},"example":{"linked":true}}}},"400":{"description":"The body does not match the schema, or the user id is an email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the events:write scope, the website does not belong to the key, or the site is not accepting events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"The user or the visitor already has the maximum number of links.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["Ingestion"],"summary":"Forget a user","description":"Removes every link of a user id and the user id on its payments, for a deletion request. The visits stay as pseudonymous visits and the payments keep their amounts.","security":[{"ApiKeyAuth":[]}],"x-required-scope":"events:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["websiteId","userId"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64},"userId":{"type":"string","minLength":1,"maxLength":128}}},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","userId":"usr_8f3k2"}}}},"responses":{"200":{"description":"The links were removed.","content":{"application/json":{"schema":{"type":"object","properties":{"removedLinks":{"type":"integer"},"unlinkedPayments":{"type":"integer"}}},"example":{"removedLinks":2,"unlinkedPayments":1}}}},"400":{"description":"The body does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the events:write scope, or the website does not belong to the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/crawls":{"post":{"tags":["Ingestion"],"summary":"Record AI crawler requests","description":"Sends the requests your server received from AI crawlers, which run no JavaScript and so never load the tracker. Only a user agent of a known AI crawler is recorded; anything else is counted as ignored and dropped. A crawler request is not a visitor, a pageview or a billable event.","security":[{"ApiKeyAuth":[]}],"x-required-scope":"events:write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["websiteId","hits"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64},"hits":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["userAgent","path"],"properties":{"userAgent":{"type":"string","maxLength":1024},"path":{"type":"string","maxLength":2048},"timestamp":{"type":"integer","description":"Epoch milliseconds of the request. Defaults to now."}}}}}},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","hits":[{"userAgent":"Mozilla/5.0 (compatible; GPTBot/1.2)","path":"/pricing"}]}}}},"responses":{"202":{"description":"The requests were counted.","content":{"application/json":{"schema":{"type":"object","properties":{"recorded":{"type":"integer"},"ignored":{"type":"integer"}}},"example":{"recorded":1,"ignored":0}}}},"400":{"description":"The body does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the events:write scope, the website does not belong to the key, or the site is not accepting events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many requests for this key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/ad-spend":{"post":{"tags":["Ingestion"],"summary":"Record ad spend","description":"Sends what you spent on ads per day, source, campaign and currency, so the revenue page can show the return on it. The source and the campaign are the utm_source and utm_campaign of your ads. Sending the same day, source, campaign and currency again replaces the amount.","security":[{"ApiKeyAuth":[]}],"x-required-scope":"revenue:write","x-idempotency":"An entry replaces the amount of the same day, source, campaign and currency.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["websiteId","entries"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64},"entries":{"type":"array","minItems":1,"maxItems":1000,"items":{"type":"object","required":["day","source","currency","amount"],"properties":{"day":{"type":"string","format":"date"},"source":{"type":"string","minLength":1,"maxLength":120},"campaign":{"type":"string","maxLength":120,"default":""},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"amount":{"type":"integer","minimum":0,"description":"Integer minor units of the currency. 2500 is 25.00."}}}}}},"example":{"websiteId":"abk_xxxxxxxxxxxxxxxx","entries":[{"day":"2026-10-27","source":"google","campaign":"launch","currency":"USD","amount":2500}]}}}},"responses":{"200":{"description":"The spend was saved.","content":{"application/json":{"schema":{"type":"object","properties":{"saved":{"type":"integer"}}},"example":{"saved":1}}}},"400":{"description":"The body does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Ad spend is not available on the current plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the revenue:write scope, or the website does not belong to the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/reports/overview":{"get":{"tags":["Reports"],"summary":"Read the totals, the series, the goals and the revenue of a website","description":"Returns the headline numbers of one website for a date range. The key needs the `reports:read` scope and the website comes from the key, never from the query string.\n\nThe range is resolved in the timezone of the website. The start day is inclusive and the end day is exclusive.\n\n```bash\ncurl \"https://abackanalytics.com/api/v1/reports/overview?range=last_30_days\" \\\n  -H \"Authorization: Bearer abk_xxxxxxxx.secret\"\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"website\": { \"id\": \"site_...\", \"name\": \"Marketing site\", \"timezone\": \"UTC\", \"trackingId\": \"abk_xxxxxxxxxxxxxxxx\" },\n    \"range\": { \"start\": \"2026-09-23\", \"end\": \"2026-10-23\", \"days\": 30, \"timezone\": \"UTC\" },\n    \"totals\": { \"visitors\": 4210, \"pageviews\": 11894, \"sessions\": 5031, \"events\": 812 },\n    \"series\": [ { \"date\": \"2026-09-23\", \"visitors\": 121, \"pageviews\": 344, \"sessions\": 140 } ],\n    \"goals\": [],\n    \"revenue\": []\n  }\n}\n```","security":[{"ApiKeyAuth":[]}],"x-curl":"curl \"https://abackanalytics.com/api/v1/reports/overview?range=last_30_days\" -H \"Authorization: Bearer abk_xxxxxxxx.secret\"","x-required-scope":"reports:read","parameters":[{"name":"range","in":"query","required":false,"description":"The date range preset. One of: last_24_hours, today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month, last_12_months, year_to_date, custom. Defaults to last_30_days.","schema":{"type":"string","enum":["last_24_hours","today","yesterday","last_7_days","last_30_days","last_90_days","this_month","last_month","last_12_months","year_to_date","custom"],"default":"last_30_days"}},{"name":"start","in":"query","required":false,"description":"The first day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"end","in":"query","required":false,"description":"The last day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive in the request and exclusive in the answer.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"responses":{"200":{"description":"The report of the website.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverviewReport"}}}},"400":{"description":"The range preset is not one of the accepted values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the reports:read scope, or the website of the key no longer exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/v1/live":{"get":{"tags":["Reports"],"summary":"Read the activity of the last few minutes","description":"Returns the most recent events of the website, the number of visitors in the window and the time of the last event. The key needs the `reports:read` scope.\n\nThe window is at most 60 minutes and the number of events is at most 200, because the endpoint reads the event stream rather than an aggregate. It is rate limited per API key, 30 requests a minute.\n\n```bash\ncurl \"https://abackanalytics.com/api/v1/live?minutes=5&limit=50\" \\\n  -H \"Authorization: Bearer abk_xxxxxxxx.secret\"\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"website\": { \"id\": \"site_...\", \"name\": \"Marketing site\" },\n    \"window\": { \"minutes\": 5, \"since\": 1790000000000, \"until\": 1790000300000 },\n    \"visitors\": 12,\n    \"lastEventAt\": 1790000299000,\n    \"events\": [\n      { \"eventId\": \"evt_...\", \"eventName\": \"pageview\", \"eventType\": \"pageview\", \"path\": \"/pricing\", \"source\": \"twitter\", \"country\": \"US\", \"device\": \"desktop\", \"occurredAt\": 1790000299000 }\n    ]\n  }\n}\n```","security":[{"ApiKeyAuth":[]}],"x-curl":"curl \"https://abackanalytics.com/api/v1/live?minutes=5&limit=50\" -H \"Authorization: Bearer abk_xxxxxxxx.secret\"","x-required-scope":"reports:read","x-rate-limit":{"limit":30,"periodSeconds":60,"key":"client address","endpoints":["GET /api/account/sites"]},"parameters":[{"name":"minutes","in":"query","required":false,"description":"The size of the window in minutes. Between 1 and 60. Defaults to 5.","schema":{"type":"integer","minimum":1,"maximum":60,"default":5}},{"name":"limit","in":"query","required":false,"description":"The number of events to return. Between 1 and 200. Defaults to 50.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"The activity of the window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuccessEnvelope"}}}},"400":{"description":"The window or the number of events is out of range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/reports/breakdown":{"get":{"tags":["Reports"],"summary":"Break one dimension down for a date range","description":"Returns the rows of one dimension for the same range as the overview endpoint. The key needs the `reports:read` scope.\n\nThe dimension is checked against an allowlist. An unknown dimension is refused with an invalid request error.\n\n```bash\ncurl \"https://abackanalytics.com/api/v1/reports/breakdown?dimension=source&range=last_30_days&limit=10\" \\\n  -H \"Authorization: Bearer abk_xxxxxxxx.secret\"\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"website\": { \"id\": \"site_...\", \"name\": \"Marketing site\" },\n    \"range\": { \"start\": \"2026-09-23\", \"end\": \"2026-10-23\", \"days\": 30 },\n    \"dimension\": \"source\",\n    \"rows\": [\n      { \"value\": \"google\", \"visitors\": 1904, \"pageviews\": 5210, \"sessions\": 2210, \"events\": 402 }\n    ]\n  }\n}\n```","security":[{"ApiKeyAuth":[]}],"x-curl":"curl \"https://abackanalytics.com/api/v1/reports/breakdown?dimension=source&range=last_30_days&limit=10\" -H \"Authorization: Bearer abk_xxxxxxxx.secret\"","x-required-scope":"reports:read","parameters":[{"name":"dimension","in":"query","required":false,"description":"The dimension to break down. One of: source, medium, campaign, channel, referrer_host, path, landing_page, country, device, browser, os, screen, event_name, term, content. Defaults to source.","schema":{"type":"string","enum":["source","medium","campaign","channel","referrer_host","path","landing_page","country","device","browser","os","screen","event_name","term","content"],"default":"source"}},{"name":"range","in":"query","required":false,"description":"The date range preset. One of: last_24_hours, today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month, last_12_months, year_to_date, custom. Defaults to last_30_days.","schema":{"type":"string","enum":["last_24_hours","today","yesterday","last_7_days","last_30_days","last_90_days","this_month","last_month","last_12_months","year_to_date","custom"],"default":"last_30_days"}},{"name":"limit","in":"query","required":false,"description":"The number of rows to return. Between 1 and 200. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":200,"default":25}},{"name":"start","in":"query","required":false,"description":"The first day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"end","in":"query","required":false,"description":"The last day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive in the request and exclusive in the answer.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"responses":{"200":{"description":"The rows of the dimension.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakdownReport"}}}},"400":{"description":"The dimension or the range preset is not one of the accepted values.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The Authorization header is missing or the key is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The key is missing the reports:read scope, or the website of the key no longer exists.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/sites/{siteId}/export":{"get":{"tags":["Account"],"summary":"Export the raw events of a website as CSV","description":"Downloads the stored events of one website as a CSV file. This is the only endpoint that does not answer with the JSON envelope, because it answers with the file itself.\n\nThe endpoint accepts two credentials. A signed in user, where the session cookie is the credential, so the dashboard can download the file. Or an API key with the reports:read scope, so a scheduled job can fetch it. A key belongs to one website, so a key may only export that website.\n\nThe account is resolved from the credential, the website id is checked against it, and there is no way to ask for the websites of another account.\n\n```bash\ncurl \"https://abackanalytics.com/api/sites/site_.../export?range=last_30_days\" \\\n  -H \"authorization: Bearer abk_...\" \\\n  -o events.csv\n```\n\nThe response carries the file name in the content-disposition header, the number of rows in x-export-rows, and a flag in x-export-truncated that says whether the export stopped at the row limit.\n\n```csv\nevent_id,event_name,event_type,occurred_at,day,path,title,source,medium,campaign,channel,referrer_host,country,device,browser,os,screen,visitor_id,session_id,revenue_minor,currency,properties\nevt_9f2c41b7ac0d4e6f8a1b,pageview,pageview,1790000000000,2026-09-23,/pricing,Pricing,twitter,,,Social,t.co,US,desktop,Chrome,macOS,lg,,,,\n```","security":[{"ApiKeyAuth":[]},{"SessionCookie":[]}],"x-curl":"curl \"https://abackanalytics.com/api/sites/site_.../export?range=last_30_days\" -H \"authorization: Bearer abk_...\" -o events.csv","x-rate-limit":{"limit":6,"periodSeconds":60,"key":"client address","endpoints":["GET /api/sites/{siteId}/export"]},"x-limits":"The export returns at most 100000 rows, and never more rows than the plan of the account allows.","parameters":[{"name":"siteId","in":"path","required":true,"description":"The identifier of the website, the same value that appears in the site URL of the dashboard.","schema":{"type":"string"}},{"name":"range","in":"query","required":false,"description":"The date range preset. One of: last_24_hours, today, yesterday, last_7_days, last_30_days, last_90_days, this_month, last_month, last_12_months, year_to_date, custom. Defaults to last_30_days. An unknown preset falls back to last_30_days. The export covers whole days, so last_24_hours is read as today.","schema":{"type":"string","enum":["last_24_hours","today","yesterday","last_7_days","last_30_days","last_90_days","this_month","last_month","last_12_months","year_to_date","custom"],"default":"last_30_days"}},{"name":"start","in":"query","required":false,"description":"The first day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},{"name":"end","in":"query","required":false,"description":"The last day of the range as YYYY-MM-DD. Only used when range is custom. The day is inclusive in the request and exclusive in the answer.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}}],"responses":{"200":{"description":"The CSV file with the events of the range.","headers":{"content-disposition":{"description":"The suggested file name.","schema":{"type":"string"}},"x-export-rows":{"description":"The number of rows in the file.","schema":{"type":"string"}},"x-export-truncated":{"description":"True when the export stopped at the row limit, so the file is a part of the range.","schema":{"type":"string"}}},"content":{"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"The date range is empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"The session cookie is missing or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"CSV export is not available on the current plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The signed in user is not a member of the account of that website.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The website does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many export requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/account/sites":{"get":{"tags":["Account"],"summary":"List the websites of the account of the signed in user","description":"Returns the websites of the account the signed in user belongs to. The account is resolved from the session on the server. There is no account parameter in the query string, so there is no way to ask for the websites of another account.\n\nThe answer carries the public tracking id of each website, so a tool can be pointed at the right site without opening the dashboard. It never carries a secret: no visitor salt, no API key and no integration credential.\n\n```bash\ncurl \"https://abackanalytics.com/api/account/sites\" \\\n  -H \"Cookie: abk_session=...\"\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"websites\": [\n      {\n        \"id\": \"site_...\",\n        \"name\": \"Marketing site\",\n        \"domain\": \"example.com\",\n        \"timezone\": \"UTC\",\n        \"status\": \"active\",\n        \"trackingId\": \"abk_xxxxxxxxxxxxxxxx\",\n        \"retentionDays\": 30\n      }\n    ]\n  }\n}\n```","security":[{"SessionCookie":[]}],"x-curl":"curl \"https://abackanalytics.com/api/account/sites\" -H \"Cookie: abk_session=...\"","x-rate-limit":{"limit":30,"periodSeconds":60,"key":"client address","endpoints":["GET /api/account/sites"]},"responses":{"200":{"description":"The websites of the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountSites"}}}},"401":{"description":"The session cookie is missing or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"The signed in user has no account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/auth/signup":{"post":{"tags":["Auth"],"summary":"Create an account and sign in","description":"Creates a user, an account, an owner membership and a free plan, and answers with a session cookie.\n\nThe request must come from the same origin as the application. A request with an unexpected origin is refused.\n\n```bash\ncurl -X POST https://abackanalytics.com/api/auth/signup \\\n  -H \"content-type: application/json\" \\\n  -H \"Origin: https://abackanalytics.com\" \\\n  -d '{\n    \"email\": \"founder@example.com\",\n    \"password\": \"a-long-passphrase\",\n    \"name\": \"Ada\",\n    \"accountName\": \"Example Inc\"\n  }'\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"user\": { \"id\": \"usr_...\", \"email\": \"founder@example.com\", \"name\": \"Ada\" },\n    \"accountId\": \"acc_...\"\n  }\n}\n```","security":[],"x-curl":"curl -X POST https://abackanalytics.com/api/auth/signup -H \"content-type: application/json\" -H \"Origin: https://abackanalytics.com\" -d '{\"email\":\"founder@example.com\",\"password\":\"a-long-passphrase\",\"name\":\"Ada\"}'","x-rate-limit":{"limit":10,"periodSeconds":60,"key":"client address","endpoints":["POST /api/auth/signup","POST /api/auth/login"]},"requestBody":{"required":true,"description":"The new account. The body is at most 16384 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"},"example":{"email":"founder@example.com","password":"a-long-passphrase","name":"Ada","accountName":"Example Inc"}}}},"responses":{"201":{"description":"The account was created. The response sets the session cookie and returns the new user and account.","headers":{"set-cookie":{"description":"The session cookie of the new user.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResult"}}}},"400":{"description":"The body is not valid JSON, or the email address or the password does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The request came from an unrecognised origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"An account already exists for that email address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"The body is larger than 16384 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many attempts. Wait a minute and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/auth/login":{"post":{"tags":["Auth"],"summary":"Sign in and receive a session cookie","description":"Checks the email address and the password and answers with a session cookie. A wrong password and an unknown email answer with the same message, so the endpoint never reveals whether an account exists.\n\nThe request must come from the same origin as the application. A request with an unexpected origin is refused.\n\n```bash\ncurl -X POST https://abackanalytics.com/api/auth/login \\\n  -H \"content-type: application/json\" \\\n  -H \"Origin: https://abackanalytics.com\" \\\n  -d '{\n    \"email\": \"founder@example.com\",\n    \"password\": \"a-long-passphrase\"\n  }'\n```\n\n```json\n{\n  \"ok\": true,\n  \"data\": {\n    \"user\": { \"id\": \"usr_...\", \"email\": \"founder@example.com\", \"name\": \"Ada\" }\n  }\n}\n```","security":[],"x-curl":"curl -X POST https://abackanalytics.com/api/auth/login -H \"content-type: application/json\" -H \"Origin: https://abackanalytics.com\" -d '{\"email\":\"founder@example.com\",\"password\":\"a-long-passphrase\"}'","x-rate-limit":{"limit":10,"periodSeconds":60,"key":"client address","endpoints":["POST /api/auth/signup","POST /api/auth/login"]},"requestBody":{"required":true,"description":"The credentials. The body is at most 8192 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"},"example":{"email":"founder@example.com","password":"a-long-passphrase"}}}},"responses":{"200":{"description":"The credentials are correct. The response sets the session cookie.","headers":{"set-cookie":{"description":"The session cookie of the signed in user.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResult"}}}},"400":{"description":"The body is not valid JSON or the email address is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"That email address and password do not match.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"The request came from an unrecognised origin.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"The body is larger than 8192 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Too many attempts. Wait a minute and try again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/openapi":{"get":{"tags":["Meta"],"summary":"This document","description":"Returns the OpenAPI 3.1 description of the API as JSON. The endpoint is public and carries no tenant data.\n\n```bash\ncurl https://abackanalytics.com/api/openapi\n```","security":[],"x-curl":"curl https://abackanalytics.com/api/openapi","responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","description":"An Aback Analytics API key, sent as a bearer token.\n\nThe key has the shape `abk_xxxxxxxx.secret`. The part before the dot is the public key prefix, the part after the dot is the secret. The prefix identifies the key row, the secret authenticates the request. Only a keyed digest of the secret is stored, the raw key is shown exactly once when the key is created, and the key can be revoked at any time.\n\nA key belongs to one website and carries the scopes it was created with. A request that needs a scope the key does not have is refused with a forbidden error."},"SessionCookie":{"type":"apiKey","in":"cookie","name":"abk_session","description":"The session cookie of a signed in user. It is set by POST /api/auth/login and POST /api/auth/signup, it is HttpOnly and SameSite=Lax, and it expires after 30 days.\n\nThe account is always resolved from the session on the server. No endpoint that uses this credential accepts an account identifier from the client."}},"schemas":{"SuccessEnvelope":{"type":"object","description":"The envelope of every successful JSON response.","required":["ok","data"],"properties":{"ok":{"type":"boolean","const":true},"data":{"type":"object","description":"The payload of the endpoint."},"requestId":{"type":"string","description":"The correlation id of the request."}}},"ErrorEnvelope":{"type":"object","description":"The envelope of every failed JSON response. The message is safe to show to a user and never carries a stack trace, SQL or an internal identifier.","required":["ok","error"],"properties":{"ok":{"type":"boolean","const":false},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"A stable, machine readable error code.","enum":["invalid_request","validation_failed","unauthorized","plan_restricted","quota_exceeded","forbidden","not_found","conflict","payload_too_large","rate_limited","internal_error","method_not_allowed"]},"message":{"type":"string","description":"A safe human readable message."},"details":{"description":"Optional detail about the failure. Validation failures list the field and the reason for each issue.","oneOf":[{"type":"object"},{"type":"array","items":{"type":"object"}},{"type":"string"}]}}},"requestId":{"type":"string","description":"The correlation id of the request. Quote it when you report a problem."}}},"CollectRequest":{"type":"object","required":["websiteId","events"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64,"description":"The public tracking id of the website. It is not a secret.","examples":["abk_xxxxxxxxxxxxxxxx"]},"events":{"type":"array","minItems":1,"maxItems":20,"description":"The batch of browser events.","items":{"$ref":"#/components/schemas/BrowserEvent"}}}},"BrowserEvent":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string","minLength":8,"maxLength":64,"description":"The idempotency key of the event, generated by the client. The server stores each id once, so a retried batch is never counted twice."},"type":{"type":"string","enum":["pageview","event"],"description":"The kind of event. A pageview needs no name, a custom event does."},"name":{"type":"string","maxLength":64,"description":"The event name of a custom event. Letters, numbers, dot, underscore, colon and dash are allowed."},"url":{"type":"string","maxLength":2048,"description":"The full URL of the page. The query string is sanitised on the server, so only the query parameters the site allows survive."},"title":{"type":"string","maxLength":200,"description":"The title of the page."},"referrer":{"type":"string","maxLength":2048,"description":"The referring URL, when the browser sends one."},"properties":{"type":"object","description":"Optional event properties. At most 20 keys, a key is at most 40 characters, a string value is at most 256 characters, and the whole payload is at most 4096 bytes. A key that looks sensitive is dropped before the event is stored.","additionalProperties":{"description":"A property value. A string, a number, a boolean or null. Objects and arrays are dropped rather than stored.","oneOf":[{"type":"string","maxLength":256},{"type":"number"},{"type":"boolean"},{"type":"null"}]}},"timestamp":{"type":"integer","description":"The epoch milliseconds of the event. The server clamps it, so a wrong clock does not move a number into the past."},"screenWidth":{"type":"integer","minimum":0,"maximum":20000,"description":"The viewport width in CSS pixels. The server buckets it."},"language":{"type":"string","maxLength":32,"description":"The language of the page."},"visitorId":{"type":"string","minLength":16,"maxLength":64,"description":"The pseudonymous first party identifier of the visitor. An event without one of at least 16 characters is rejected."}}},"CollectResult":{"type":"object","required":["accepted","rejected"],"properties":{"accepted":{"type":"integer","description":"The number of events that were accepted and queued for processing."},"rejected":{"type":"integer","description":"The number of events that were not recorded."}}},"ServerEventRequest":{"type":"object","required":["websiteId","event"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64,"description":"The public tracking id of the website the key belongs to."},"event":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$","description":"The name of the event."},"properties":{"type":"object","description":"Optional event properties. At most 20 keys, a key is at most 40 characters, a string value is at most 256 characters, and the whole payload is at most 4096 bytes. A key that looks sensitive is dropped before the event is stored.","additionalProperties":{"description":"A property value. A string, a number, a boolean or null. Objects and arrays are dropped rather than stored.","oneOf":[{"type":"string","maxLength":256},{"type":"number"},{"type":"boolean"},{"type":"null"}]}},"timestamp":{"type":"integer","description":"The epoch milliseconds of the event. The server clamps it."},"visitorId":{"type":"string","minLength":16,"maxLength":64,"description":"The identifier of the visitor browser, to link the event to a visitor."},"sessionId":{"type":"string","minLength":8,"maxLength":64,"description":"The identifier of a known session, to link the event to a session."},"path":{"type":"string","maxLength":2048,"description":"The page path the event happened on."}}},"ServerEventResult":{"type":"object","required":["eventId","status"],"properties":{"eventId":{"type":"string","description":"The identifier of the stored event."},"status":{"type":"string","enum":["accepted","duplicate"],"description":"Accepted for a new event, duplicate when the event was already stored."}}},"RevenueEventRequest":{"type":"object","required":["websiteId","idempotencyKey","amount","currency"],"properties":{"websiteId":{"type":"string","minLength":6,"maxLength":64,"description":"The public tracking id of the website the key belongs to."},"idempotencyKey":{"type":"string","minLength":8,"maxLength":128,"description":"The key that makes the call safe to retry. Repeating it never counts the payment twice."},"provider":{"type":"string","minLength":2,"maxLength":48,"default":"manual","description":"The name of the system that confirmed the payment."},"type":{"type":"string","enum":["payment","refund","subscription","chargeback"],"default":"payment","description":"The kind of money movement."},"amount":{"type":"integer","minimum":-100000000,"maximum":100000000,"description":"The amount in integer minor units. 1999 means 19.99."},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"The three letter ISO code of the currency."},"customerRef":{"type":"string","maxLength":128,"description":"An opaque reference to the customer. An email address is not accepted and is never stored."},"productId":{"type":"string","maxLength":128,"description":"The product or plan the payment is for."},"transactionId":{"type":"string","minLength":8,"maxLength":128,"description":"For a refund or a chargeback, the idempotency key of the payment it reverses. The reversal then reduces the revenue of that payment's source, even when it is sent before the payment."},"userId":{"type":"string","minLength":1,"maxLength":128,"description":"Your own id for the user who paid, as sent to identify. The payment is attributed to the visits of every browser linked to that user. An email address is refused."},"visitorId":{"type":"string","minLength":16,"maxLength":64,"description":"The identifier of the visitor browser. With it, the payment is attributed to a touchpoint."},"sessionId":{"type":"string","minLength":8,"maxLength":64,"description":"The identifier of a recorded session of the website. That session is the touchpoint. An id that matches no session is ignored."},"attributionId":{"type":"string","maxLength":128,"description":"An opaque attribution reference issued by the browser SDK."},"timestamp":{"type":"integer","description":"The epoch milliseconds of the payment. The server uses the current time when it is missing. A time before 2000-01-01, which is what a time in seconds looks like, or more than a day in the future is refused with validation_failed."},"properties":{"type":"object","description":"Optional event properties. At most 20 keys, a key is at most 40 characters, a string value is at most 256 characters, and the whole payload is at most 4096 bytes. A key that looks sensitive is dropped before the event is stored.","additionalProperties":{"description":"A property value. A string, a number, a boolean or null. Objects and arrays are dropped rather than stored.","oneOf":[{"type":"string","maxLength":256},{"type":"number"},{"type":"boolean"},{"type":"null"}]}}}},"RevenueEventResult":{"type":"object","required":["revenueEventId","status","attributed"],"properties":{"revenueEventId":{"type":"string","description":"The identifier of the revenue event."},"status":{"type":"string","enum":["recorded","duplicate"],"description":"Recorded for a new payment, duplicate when the idempotency key was already used."},"attributed":{"type":"boolean","description":"True when a touchpoint was found for the visitor inside the attribution window of the website."}}},"ReportRange":{"type":"object","required":["start","end","days"],"properties":{"start":{"type":"string","description":"The first day of the range in the timezone of the website. Inclusive."},"end":{"type":"string","description":"The day after the last day of the range. Exclusive."},"days":{"type":"integer","description":"The number of calendar days covered."},"timezone":{"type":"string","description":"The timezone the days were counted in."}}},"ReportWebsite":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","description":"The identifier of the website."},"name":{"type":"string","description":"The name of the website."},"timezone":{"type":"string","description":"The timezone the days of the website are counted in."}}},"Totals":{"type":"object","properties":{"visitors":{"type":"integer","description":"The unique visitors in the range."},"pageviews":{"type":"integer","description":"The recorded pageviews in the range."},"sessions":{"type":"integer","description":"The recorded sessions in the range."},"events":{"type":"integer","description":"The recorded custom events in the range."},"newVisitors":{"type":"integer","description":"The visitors that were seen for the first time in the range."},"returningVisitors":{"type":"integer","description":"The visitors that were seen in an earlier period."},"bounceRate":{"type":["number","null"],"description":"The share of sessions with a single pageview and no custom event. Null when there is no session."},"averageSessionSeconds":{"type":["number","null"],"description":"The mean duration of the sessions with more than one pageview or event. Null when there is no such session."},"viewsPerVisitor":{"type":["number","null"],"description":"The pageviews per visitor. Null when there is no visitor."}}},"TimeSeriesPoint":{"type":"object","required":["date","visitors","pageviews","sessions"],"properties":{"date":{"type":"string","description":"The day of the point."},"visitors":{"type":"integer"},"pageviews":{"type":"integer"},"sessions":{"type":"integer"}}},"GoalStat":{"type":"object","required":["id","name","eventName","kind","conversions","visitors"],"properties":{"id":{"type":"string"},"name":{"type":"string","description":"The name of the goal."},"eventName":{"type":"string","description":"The event name or the page path the goal matches."},"kind":{"type":"string","enum":["event","page"],"description":"Whether the goal matches an event name or a page path."},"conversions":{"type":"integer","description":"The number of conversions."},"visitors":{"type":"integer","description":"The number of visitors that converted."},"conversionRate":{"type":["number","null"],"description":"The conversions divided by the visitors of the range. Null when there is no visitor."}}},"RevenueSummary":{"type":"object","required":["currency","grossMinor","refundsMinor","netMinor","payments","customers"],"properties":{"currency":{"type":"string","description":"The currency of the row. Currencies are never mixed in one row."},"grossMinor":{"type":"integer","description":"The gross amount in minor units, before refunds."},"refundsMinor":{"type":"integer","description":"The refunded amount in minor units."},"netMinor":{"type":"integer","description":"The net amount in minor units, after refunds."},"payments":{"type":"integer","description":"The number of payments."},"customers":{"type":"integer","description":"The number of paying customers."}}},"OverviewReport":{"type":"object","required":["website","range","totals","series","goals","revenue"],"properties":{"website":{"$ref":"#/components/schemas/ReportWebsite"},"range":{"$ref":"#/components/schemas/ReportRange"},"totals":{"$ref":"#/components/schemas/Totals"},"series":{"type":"array","items":{"$ref":"#/components/schemas/TimeSeriesPoint"}},"goals":{"type":"array","items":{"$ref":"#/components/schemas/GoalStat"}},"revenue":{"type":"array","items":{"$ref":"#/components/schemas/RevenueSummary"}}}},"BreakdownRow":{"type":"object","required":["value","visitors","pageviews","sessions","events"],"properties":{"value":{"type":"string","description":"The value of the dimension."},"visitors":{"type":"integer"},"pageviews":{"type":"integer"},"sessions":{"type":"integer"},"events":{"type":"integer"}}},"BreakdownReport":{"type":"object","required":["website","range","dimension","rows"],"properties":{"website":{"$ref":"#/components/schemas/ReportWebsite"},"range":{"$ref":"#/components/schemas/ReportRange"},"dimension":{"type":"string","description":"The dimension that was broken down."},"rows":{"type":"array","items":{"$ref":"#/components/schemas/BreakdownRow"}}}},"AccountSite":{"type":"object","required":["id","name","domain","timezone","status","trackingId","retentionDays"],"properties":{"id":{"type":"string","description":"The identifier of the website."},"name":{"type":"string","description":"The name of the website."},"domain":{"type":"string","description":"The canonical domain of the website. Empty when the site has none yet."},"timezone":{"type":"string","description":"The timezone the days of the website are counted in."},"status":{"type":"string","enum":["active","paused","archived"],"description":"The status of the website."},"trackingId":{"type":"string","description":"The public tracking id of the website. It is not a secret and it belongs in the browser snippet."},"retentionDays":{"type":"integer","description":"The number of days the raw events of the website are kept."}}},"AccountSites":{"type":"object","required":["websites"],"properties":{"websites":{"type":"array","description":"The websites of the account of the signed in user.","items":{"$ref":"#/components/schemas/AccountSite"}}}},"SignupRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","maxLength":254,"format":"email","description":"The email address of the new user."},"password":{"type":"string","minLength":10,"maxLength":200,"description":"The password of the new user. At least 10 characters."},"name":{"type":"string","maxLength":80,"description":"The name of the new user."},"accountName":{"type":"string","minLength":1,"maxLength":80,"description":"The name of the account. The name of the user is used when it is missing."}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","maxLength":254,"format":"email","description":"The email address of the account."},"password":{"type":"string","minLength":1,"maxLength":200,"description":"The password of the account."}}},"AuthUser":{"type":"object","required":["id","email","name"],"properties":{"id":{"type":"string","description":"The identifier of the user."},"email":{"type":"string","description":"The email address of the user."},"name":{"type":"string","description":"The name of the user."}}},"SignupResult":{"type":"object","required":["user","accountId"],"properties":{"user":{"$ref":"#/components/schemas/AuthUser"},"accountId":{"type":"string","description":"The identifier of the new account."}}},"LoginResult":{"type":"object","required":["user"],"properties":{"user":{"$ref":"#/components/schemas/AuthUser"}}}},"responses":{"BadRequest":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationFailed":{"description":"The body does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"The credential is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"The credential is valid but not allowed to do this.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"The resource does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Conflict":{"description":"The request conflicts with an existing record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PayloadTooLarge":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PlanRestricted":{"description":"The plan does not include this feature.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"QuotaExceeded":{"description":"The monthly limit of the plan is reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Too many requests.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Something failed on the server.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"x-error-codes":{"invalid_request":{"status":400,"meaning":"The request itself is wrong. An unknown date range, an unknown dimension, a body that is not valid JSON or a cross origin write all land here."},"validation_failed":{"status":400,"meaning":"The body did not match the schema. The details list the field and the reason for each issue."},"unauthorized":{"status":401,"meaning":"The Authorization header is missing, the key is not valid, expired or revoked, or the session cookie is missing or expired."},"plan_restricted":{"status":402,"meaning":"The plan of the account does not include the feature the endpoint needs."},"quota_exceeded":{"status":402,"meaning":"The monthly event limit of the plan has been reached."},"forbidden":{"status":403,"meaning":"The key is valid but the scope is missing, the website in the body does not belong to the key, the origin is not allowed, or the signed in user may not act on this resource."},"not_found":{"status":404,"meaning":"The resource does not exist, or the caller may not know that it exists."},"conflict":{"status":409,"meaning":"The request conflicts with an existing record, for example an account that already exists for that email address."},"payload_too_large":{"status":413,"meaning":"The body is larger than the endpoint accepts. The body is refused before it is parsed."},"rate_limited":{"status":429,"meaning":"Too many requests. The response carries a Retry-After header of 60 seconds."},"internal_error":{"status":500,"meaning":"Something failed on the server. The message is generic on purpose. Quote the request id when you report it."},"method_not_allowed":{"status":405,"meaning":"Reserved. No endpoint in this version answers with this code, but it is part of the stable error code set."}},"x-rate-limits":{"description":"A limit is applied per bucket. The key of the bucket decides who shares the counter.\n\nWhen a limit is exceeded the endpoint answers 429 with the error code rate_limited and a Retry-After header of 60 seconds.\n\nThe server to server endpoints POST /api/v1/events, POST /api/v1/revenue, GET /api/v1/reports/overview and GET /api/v1/reports/breakdown do not apply a request rate limit in this version. The monthly event limit of the plan still applies to the events that reach the product.","buckets":{"ingest":{"limit":120,"periodSeconds":60,"key":"website id and client address","endpoints":["POST /api/collect"]},"auth":{"limit":10,"periodSeconds":60,"key":"client address","endpoints":["POST /api/auth/signup","POST /api/auth/login"]},"admin":{"limit":30,"periodSeconds":60,"key":"client address","endpoints":["GET /api/account/sites"]},"export":{"limit":6,"periodSeconds":60,"key":"client address","endpoints":["GET /api/sites/{siteId}/export"]}}},"x-idempotency":{"description":"Three endpoints have an idempotency contract and they work in three different ways.\n\nPOST /api/collect uses the client generated id of each browser event. The server stores the pair of website and event id once, so a retried batch is never counted twice.\n\nPOST /api/v1/revenue uses the idempotency key of the payment. The key is stored with the payment, so a retry answers 200 with the status duplicate and no number changes.\n\nPOST /api/v1/events has no client idempotency key. The server generates the event id, so a retry records a second event. Send one request per event and keep the answer."},"x-limits":{"description":"The hard limits the server enforces. A request that breaks one is refused before it is stored.","values":{"collectBodyBytes":32768,"serverBodyBytes":32768,"signupBodyBytes":16384,"loginBodyBytes":8192,"maxEventsPerRequest":20,"maxEventNameLength":64,"maxUrlLength":2048,"maxTitleLength":200,"maxPropertyKeys":20,"maxPropertyKeyLength":40,"maxPropertyValueLength":256,"maxPropertiesBytes":4096,"maxRangeDays":730,"breakdownLimitMin":1,"breakdownLimitMax":200,"breakdownLimitDefault":25,"exportRowsMax":100000}}}