{"openapi":"3.1.0","info":{"title":"Adrails Tracking API","version":"1.0.0","description":"First-party tracking for your own site and server. The script records visits and the campaign, ad and click id they arrived with; the collect endpoint records events such as purchases and leads. Only events your server signs with the server key are forwarded to Meta and Google Ads.\n\nEvery field, limit and error on these pages is checked against the code that serves them, by a test in the Adrails repository."},"servers":[{"url":"https://adrails.ai"}],"tags":[{"name":"Tracking","description":"The two endpoints your site and server call."}],"paths":{"/api/tracking/script":{"get":{"operationId":"getTrackingScript","tags":["Tracking"],"summary":"Load the tracking script","description":"Returns the JavaScript tracker for a tracking source. Load it on every page with a script tag. On load it stores a visitor id, keeps the UTM parameters and click ids the visitor arrived with for 90 days, sends a `page_view` event, and defines `window.adrailsTrack(eventName, fields)`.\n\nThe response is public and cacheable for 5 minutes (`Cache-Control: public, max-age=300, stale-while-revalidate=3600`) and is served with `Cross-Origin-Resource-Policy: cross-origin` so any site can embed it.","security":[],"parameters":[{"name":"key","in":"query","required":true,"description":"The public key of your tracking source, shown in Settings > Tracking. It starts with `atr_`.","schema":{"type":"string","pattern":"^atr_[A-Za-z0-9_-]{20,80}$"},"example":"atr_Xk2v9QmZr8LpT4wNc7Ha1BdE"}],"responses":{"200":{"description":"The tracker script.","headers":{"Cache-Control":{"description":"`public, max-age=300, stale-while-revalidate=3600`","schema":{"type":"string"}}},"content":{"application/javascript":{"schema":{"type":"string"},"example":"(()=>{ /* tracker */ })();"}}},"400":{"description":"The key is missing or is not a tracking key. Only its format is checked here: a well-formed key that belongs to no source still returns the script, and its events are refused by the collect endpoint.","content":{"text/plain":{"schema":{"type":"string"},"example":"Invalid tracking key."}}}},"x-codeSamples":[{"id":"html","label":"HTML","lang":"html","source":"<script async src=\"https://adrails.ai/api/tracking/script?key=<public key>\"></script>"},{"id":"js","lang":"js","source":false},{"id":"python","lang":"python","source":false}]}},"/api/tracking/collect":{"options":{"operationId":"preflightTrackingEvent","tags":["Tracking"],"summary":"CORS preflight","description":"Answered for browsers before a cross-origin `POST`. It succeeds when the key names a source and the `Origin` is one of its allowed domains. The answer allows the methods `POST, OPTIONS` and the headers `authorization, content-type, x-adrails-key`, echoes the allowed origin in `Access-Control-Allow-Origin`, and may be cached for 86,400 seconds. You never call it yourself.","security":[],"parameters":[{"name":"key","in":"query","required":true,"description":"The public key of your tracking source (`atr_...`), shown in Settings > Tracking. You can send it in the `x-adrails-key` header instead.","schema":{"type":"string","pattern":"^atr_[A-Za-z0-9_-]{20,80}$"},"example":"atr_Xk2v9QmZr8LpT4wNc7Ha1BdE"},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"},"example":"https://shop.example.com"}],"responses":{"204":{"description":"The origin may post events to this source.","headers":{"Access-Control-Allow-Origin":{"schema":{"type":"string"},"description":"The request's `Origin`."},"Access-Control-Allow-Methods":{"schema":{"type":"string"},"description":"`POST, OPTIONS`"},"Access-Control-Allow-Headers":{"schema":{"type":"string"},"description":"`authorization, content-type, x-adrails-key`"},"Access-Control-Max-Age":{"schema":{"type":"string"},"description":"`86400`"}}},"403":{"description":"No source has this key, or the origin is not one of its allowed domains. The answer has no body and no `Access-Control-Allow-Origin`."}},"x-codeSamples":[{"id":"curl","label":"cURL","lang":"bash","source":"curl -i -X OPTIONS \"https://adrails.ai/api/tracking/collect?key=<public key>\" \\\n  -H \"Origin: https://shop.example.com\" \\\n  -H \"Access-Control-Request-Method: POST\" \\\n  -H \"Access-Control-Request-Headers: content-type\""},{"id":"js","lang":"js","source":false},{"id":"python","lang":"python","source":false}]},"post":{"operationId":"collectTrackingEvent","tags":["Tracking"],"summary":"Record an event","description":"Records one event for a tracking source. Name the source with its public key, in the `x-adrails-key` header or the `key` query parameter.\n\nA call is accepted in one of two ways:\n\n- **From a browser**, when the `Origin` header is one of the source's allowed domains or a subdomain of one. These events are recorded in Adrails for attribution and are never sent to Meta or Google Ads. A browser caller is limited to 120 events per minute per visitor address.\n- **From your server**, with `Authorization: Bearer <server key>`. Signed events are recorded and, when the source is set up for it, forwarded to Meta's Conversions API and Google Ads within the source's limits. Server calls are not rate limited.\n\nThe body is at most 64,000 bytes. An `event_id` already recorded for the source is answered with `200` and `duplicate: true`, and nothing is recorded or forwarded a second time.","security":[{"serverKey":[]},{}],"parameters":[{"name":"key","in":"query","required":true,"description":"The public key of your tracking source (`atr_...`), shown in Settings > Tracking. You can send it in the `x-adrails-key` header instead.","schema":{"type":"string","pattern":"^atr_[A-Za-z0-9_-]{20,80}$"},"example":"atr_Xk2v9QmZr8LpT4wNc7Ha1BdE"},{"name":"Origin","in":"header","required":false,"description":"Sent by the browser. Must match an allowed domain of the source when the call is not signed with the server key.","schema":{"type":"string","format":"uri"},"example":"https://shop.example.com"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttributionEvent"},"examples":{"serverPurchase":{"summary":"Purchase from your server, forwarded to Meta and Google Ads","value":{"event_name":"purchase","event_id":"order-1042","value":89.9,"currency":"EUR","fbc":"fb.1.1759313000000.IwAR2xYz","fbp":"fb.1.1759312000000.1234567890","gclid":"Cj0KCQjw9Kk","user":{"email":"customer@example.com","external_id":"customer-381"}}},"appSignup":{"summary":"Sign-up from an app backend with an explicit time","value":{"event_name":"signup","event_id":"user-381-signup","action_source":"app","event_time":"2026-10-01T12:00:00Z","user":{"external_id":"user-381"}}}}}}},"responses":{"200":{"description":"This `event_id` was already recorded for the source. Nothing was recorded or forwarded again.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"},"example":{"accepted":true,"duplicate":true,"eventId":"order-1042"}}}},"202":{"description":"The event was recorded. Whether it reaches Meta or Google Ads is decided afterwards and shown in Settings > Tracking, not in this response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"},"example":{"accepted":true,"duplicate":false,"eventId":"order-1042"}}}},"400":{"description":"The body is not JSON, or does not match the event schema. `details` lists the fields that failed, as `fieldErrors`.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/ValidationError"}]},"examples":{"notJson":{"summary":"Not JSON","value":{"error":"The tracking event is not valid JSON."}},"invalid":{"summary":"Invalid event","value":{"details":{"formErrors":[],"fieldErrors":{"event_id":["String must contain at least 8 character(s)"],"event_name":["Invalid"]}},"error":"The tracking event is invalid."}}}}}},"403":{"description":"The call is not signed with the server key and its `Origin` is not an allowed domain of the source.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This origin is not allowed for the tracking source."}}}},"404":{"description":"No tracking source has this public key. This response carries no CORS headers, so a browser cannot read it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Tracking source not found."}}}},"413":{"description":"The body is larger than 64,000 bytes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"The tracking event is too large."}}}},"429":{"description":"A browser caller sent more than 120 events to this source in a minute from the same address. Server-signed calls are never limited.","headers":{"Retry-After":{"description":"Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Too many tracking events from this visitor."}}}}}}}},"components":{"securitySchemes":{"serverKey":{"type":"http","scheme":"bearer","description":"The source's server key (`atr_server_...`), shown to workspace administrators in Settings > Tracking. Send it from your server only, never from a page. Without it, a call is accepted only from an allowed domain, and its events are never forwarded to Meta or Google Ads."}},"schemas":{"AttributionEvent":{"type":"object","description":"One event. Text fields are trimmed; an empty string is refused, so leave a field out or send `null` instead. Unknown fields are ignored.","required":["event_id","event_name"],"properties":{"event_name":{"type":"string","pattern":"^[a-z][a-z0-9_]{1,63}$","description":"Lowercase name, 2 to 64 characters of `a-z`, `0-9` and `_`, starting with a letter. These names are sent to Meta under its standard names: `page_view` (PageView), `view_content` (ViewContent), `add_to_cart` (AddToCart), `checkout_start` (InitiateCheckout), `purchase` (Purchase), `lead` (Lead), `signup` (CompleteRegistration), `subscription` (Subscribe), `trial_started` (StartTrial), `app_install` (Install). Any other name is sent to Meta as it is.","examples":["purchase"]},"event_id":{"type":"string","minLength":8,"maxLength":160,"description":"Your unique id for this event, such as an order id. A second event with the same id for the source is not recorded again. Use the id your Meta pixel sent for the same action, so Meta counts it once.","examples":["order-1042"]},"event_time":{"type":"string","format":"date-time","description":"When the event happened, as an ISO 8601 date-time. Must be within the last 7 days and at most 5 minutes ahead. Defaults to the time Adrails receives the event.","examples":["2026-10-01T12:00:00Z"]},"action_source":{"type":"string","enum":["website","app"],"default":"website","description":"Where the event happened."},"value":{"type":["number","null"],"minimum":0,"maximum":1000000000,"description":"The amount of the conversion, in `currency`, in units (89.90, not 8990). A signed event above the source's largest value per event is recorded but held back from Meta and Google Ads.","examples":[89.9]},"currency":{"type":["string","null"],"minLength":3,"maxLength":3,"description":"ISO 4217 currency code of `value`. Upper-cased by Adrails.","examples":["EUR"]},"visitor_id":{"type":["string","null"],"minLength":1,"maxLength":160,"description":"The visitor id the script stores in the `_adrails_visitor_v1` cookie. Send it from your server to tie a sale to the visit."},"landing_url":{"type":["string","null"],"format":"uri","maxLength":2000,"description":"The page the visitor landed on. Sent to Meta as `event_source_url`."},"referrer":{"type":["string","null"],"format":"uri","maxLength":2000,"description":"The page that sent the visitor."},"utm_source":{"type":["string","null"],"minLength":1,"maxLength":120},"utm_medium":{"type":["string","null"],"minLength":1,"maxLength":120},"utm_campaign":{"type":["string","null"],"minLength":1,"maxLength":300},"utm_content":{"type":["string","null"],"minLength":1,"maxLength":300},"utm_term":{"type":["string","null"],"minLength":1,"maxLength":300},"campaign_id":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"The ad platform's campaign id, when your ad URLs carry it."},"adset_id":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"The ad set or ad group id."},"ad_id":{"type":["string","null"],"minLength":1,"maxLength":200,"description":"The ad id."},"fbclid":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Meta's click id from the landing URL."},"fbc":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Meta's click cookie (`_fbc`). Sent to Meta to match the conversion to the click."},"fbp":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Meta's browser cookie (`_fbp`). Sent to Meta to match the conversion to the browser."},"gclid":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Google's click id. Required for Google Ads to credit the conversion."},"ttclid":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"TikTok's click id. Recorded for attribution."},"msclkid":{"type":["string","null"],"minLength":1,"maxLength":300,"description":"Microsoft Advertising's click id. Recorded for attribution."},"user":{"$ref":"#/components/schemas/EventUser"},"properties":{"type":"object","additionalProperties":true,"description":"Your own fields, kept with the event. At most 8,000 characters once serialized as JSON. Not sent to the ad platforms."}}},"EventUser":{"type":"object","description":"The customer, for matching. Adrails stores the email and external id only as keyed hashes and never stores the phone number. On a signed event sent to Meta, each is sent SHA-256 hashed.","properties":{"email":{"type":"string","format":"email","maxLength":320,"examples":["customer@example.com"]},"external_id":{"type":"string","minLength":1,"maxLength":300,"description":"Your id for the customer."},"phone":{"type":"string","minLength":5,"maxLength":40,"description":"Any format; only its digits are used."}}},"Accepted":{"type":"object","required":["accepted","duplicate","eventId"],"properties":{"accepted":{"type":"boolean","const":true},"duplicate":{"type":"boolean","description":"True when this `event_id` was already recorded for the source."},"eventId":{"type":"string","description":"The `event_id` of the event."}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"What went wrong, in a sentence."}}},"ValidationError":{"type":"object","required":["error","details"],"properties":{"error":{"type":"string","const":"The tracking event is invalid."},"details":{"type":"object","required":["formErrors","fieldErrors"],"properties":{"formErrors":{"type":"array","items":{"type":"string"}},"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"The messages for each field that failed, keyed by field name."}}}}}}}}