The API
Everything the Tracker web app does, you can do from your own programs — the app is a client of the same API described below.
Getting started
Every endpoint below is under one base URL:
https://app.aurelia.lt/api/v1
Authenticate with an API key, created from your account's Keys page once you are signed in. Send it as a bearer token on every request:
Authorization: Bearer trk_live_YOUR_API_KEY
A request with no key, or a revoked one, is refused before anything else runs. Every response — success or failure — is JSON. An error looks like this, always with a stable, machine-readable code:
{ "error": { "code": "not_found", "message": "contact not found" } }| Status | Code | Means |
|---|---|---|
| 400 | invalid_request | A required field is missing or malformed. |
| 400 | invalid_json | The request body is not valid JSON. |
| 401 | missing_api_key | No Authorization header was sent. |
| 401 | invalid_api_key | The key is wrong or has been revoked. |
| 401 | account_suspended | The key is valid but the account is not active. |
| 404 | not_found | Nothing with that id, or it belongs to another account — the two are indistinguishable on purpose. |
| 413 | payload_too_large | The request body was over 1 MB. |
| 429 | rate_limited | Too many requests. The response carries a Retry-After header, in seconds. |
Rate limit: 300 requests per minute per key, by default. Bulk endpoints below accept up to 500 rows in one call.
Contacts
A contact is one tracked person. Every message, link and page view eventually points back to one — or to nobody, for a link shared publicly rather than sent to a named person (see Links below).
Create or find a contact
curl -X POST https://app.aurelia.lt/api/v1/contacts \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"external_ref": "user-42", "label": "Jane Doe"}'POST /api/v1/contacts →
{ "id": 1, "external_ref": "user-42" }external_ref is optional — omit it and one is generated for you. Calling this again with the same external_ref returns the same contact rather than creating a second one. label and meta (any JSON object of your own) are both optional.
Create many at once
curl -X POST https://app.aurelia.lt/api/v1/contacts/bulk \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts": [{"external_ref": "user-42"}, {"external_ref": "user-43"}]}'POST /api/v1/contacts/bulk → up to 500 rows, never all-or-nothing:
{
"results": [
{ "ok": true, "id": 1, "external_ref": "user-42" },
{ "ok": true, "id": 2, "external_ref": "user-43" }
]
}One malformed row fails only that row — every entry carries ok, and a failed one carries error: { code, message } instead of the created fields.
Look one up
curl "https://app.aurelia.lt/api/v1/contacts?external_ref=user-42" \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/contacts?external_ref= — an exact match, never a search:
{
"id": 1,
"external_ref": "user-42",
"label": "Jane Doe",
"meta": null,
"opens": 14,
"clicks": 6,
"pageViews": 9,
"firstSeenAt": "2026-09-01T09:03:11.000Z",
"lastSeenAt": "2026-09-10T14:22:05.000Z",
"trackingOptedOutAt": null
}The journey
The whole story for one person, oldest first — opens, clicks and page views, in order.
curl "https://app.aurelia.lt/api/v1/contacts/1/journey?limit=50" \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/contacts/:id/journey →
{
"items": [
{
"source": "event",
"ts": "2026-09-10T09:04:00.000Z",
"kind": "email_open",
"subject": { "type": "message", "id": 9, "external_ref": "welcome-email" }
},
{
"source": "event",
"ts": "2026-09-10T09:06:12.000Z",
"kind": "link_click",
"subject": { "type": "link", "id": 4, "external_ref": "pricing-link" }
},
{
"source": "page_activity",
"ts": "2026-09-10T09:06:15.000Z",
"kind": "page_view",
"subject": { "type": "site", "id": 2, "external_ref": "example.com" },
"page": {
"path": "/pricing",
"started_at": "2026-09-10T09:06:15.000Z",
"ended_at": "2026-09-10T09:07:16.000Z",
"active_ms": 42000,
"total_ms": 61000,
"max_scroll_pct": 88
}
}
],
"next_cursor": "eyJ0cyI6IjIwMjYtMDktMTAifQ=="
}kind is one of email_open, link_click, page_view or session_start. next_cursor is present only when there may be more — pass it back as ?cursor= for the next page.
Erase a contact
curl -X DELETE https://app.aurelia.lt/api/v1/contacts/1 \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
DELETE /api/v1/contacts/:id →
{ "id": 1, "erased": true }This genuinely erases — every open, click and page view this person generated is gone, not hidden. It cannot be undone.
Messages
A message is one tracked email. Creating one returns a pixel_url — embed it once, near the top of the email you send, as <img src="pixel_url" width="1" height="1" alt="">.
Create a message
curl -X POST https://app.aurelia.lt/api/v1/messages \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id": 1, "external_ref": "welcome-email"}'POST /api/v1/messages →
{
"id": 9,
"external_ref": "welcome-email",
"label": null,
"pixel_url": "https://t.aurelia.lt/p/Q7mZ3xVnR9pLk2WbT6cAeg"
}Create many at once
POST /api/v1/messages/bulk — the shape a campaign sender uses. Up to 500 rows, same per-row { ok, ... } contract as Contacts above, each successful row carrying the same fields POST /api/v1/messages returns.
Read a message's stats
curl https://app.aurelia.lt/api/v1/messages/9 \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/messages/:id →
{
"id": 9,
"external_ref": "welcome-email",
"label": null,
"pixel_url": "https://t.aurelia.lt/p/Q7mZ3xVnR9pLk2WbT6cAeg",
"sent_at": "2026-09-10T09:00:00.000Z",
"opens": 14,
"opens_human": 3,
"verdicts": { "human": 3, "proxy_open": 9, "prefetch": 0, "scanner": 1, "unknown": 1 },
"first_open_at": "2026-09-10T09:04:00.000Z",
"last_open_at": "2026-09-11T08:12:00.000Z",
"drops": {
"total": 0,
"note": "events dropped for this account since the process last started — opens is a floor, not exact"
}
}Read opens and opens_human together, never one alone — see “Understanding the numbers” below.
Record when it was sent
curl -X POST https://app.aurelia.lt/api/v1/messages/9/sent \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sent_at": "2026-09-10T09:00:00.000Z"}'POST /api/v1/messages/:id/sent →
{ "id": 9, "sent_at": "2026-09-10T09:00:00.000Z", "reclassified": 14 }An open that arrives before the send time is known is still recorded, but cannot yet be judged — it shows as unknown until you call this, even after the fact, at which point every open already collected is re-judged. Omit sent_at and it defaults to now.
Links
A link records a click, then sends the person straight on — instantly, never through an interstitial page. contact_id is optional: give one for a link sent to a named person, or leave it out (or send it as null) for an open link shared publicly, whose clicks belong to nobody in particular.
Create a link
curl -X POST https://app.aurelia.lt/api/v1/links \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id": 1, "target_url": "https://example.com/pricing"}'POST /api/v1/links →
{
"id": 4,
"external_ref": "l-4",
"contact_id": 1,
"url": "https://t.aurelia.lt/l/aB3dE9fG7hK"
}Send url, never the original address — it redirects straight to target_url on click.
Create many at once
POST /api/v1/links/bulk — up to 500 rows, the same { ok, ... } contract as Contacts and Messages above.
Read a link's stats
curl https://app.aurelia.lt/api/v1/links/4 \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/links/:id →
{
"id": 4,
"external_ref": "l-4",
"contact_id": 1,
"url": "https://t.aurelia.lt/l/aB3dE9fG7hK",
"target_url": "https://example.com/pricing",
"site_id": null,
"clicks": 6,
"clicks_human": 5,
"verdicts": { "human": 5, "proxy_open": 0, "prefetch": 0, "scanner": 1, "unknown": 0 },
"first_click_at": "2026-09-10T09:06:12.000Z",
"last_click_at": "2026-09-12T11:00:00.000Z",
"distinct_clickers_30d": 4,
"country_breakdown": { "by_country": { "US": 4, "DE": 1 } },
"drops": {
"total": 0,
"note": "events dropped for this account since the process last started — clicks is a floor, not exact"
}
}distinct_clickers_30d counts distinct people, over the last 30 days, who completed the identity hop after a click — never raw IP addresses. The window is in the field's own name on purpose; see “Understanding the numbers” for the verdict breakdown, which works identically for clicks and opens.
Sites
Register a site once, then add one line of JavaScript to it to record which pages a person reads and how long they spend.
Register a site
curl -X POST https://app.aurelia.lt/api/v1/sites \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}'POST /api/v1/sites →
{
"id": 2,
"domain": "example.com",
"site_key": "kQ2m9F3pXzT8waLj4dY6Rt",
"tracking_mode": "basic"
}Registering the same domain again is safe — it returns the same site rather than creating a second one.
Read a site
curl https://app.aurelia.lt/api/v1/sites/2 \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/sites/:id → the same shape as above.
Change the tracking mode
curl -X POST https://app.aurelia.lt/api/v1/sites/2/tracking-mode \
-H "Authorization: Bearer trk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tracking_mode": "extended"}'POST /api/v1/sites/:id/tracking-mode — basic or extended:
{
"id": 2,
"domain": "example.com",
"site_key": "kQ2m9F3pXzT8waLj4dY6Rt",
"tracking_mode": "extended"
}Basic records only people who arrived through one of your tracked links. Extended also records people who simply visit the site, and retroactively attaches their browsing to their contact record if they click a tracked link within 30 days. A mode change takes up to an hour to reach an already-cached visitor.
Add the snippet to your site
<script src="https://t.aurelia.lt/s.js?k=kQ2m9F3pXzT8waLj4dY6Rt" async></script>
One line, anywhere on the page. Nothing else to configure — the tracking mode set above travels with it, and a mode change reaches this script the same way any other cached response does.
Events
One feed across every layer — opens, clicks and page views — for a poller to consume instead of pulling each resource on a timer.
curl "https://app.aurelia.lt/api/v1/events?cursor=0&limit=200" \ -H "Authorization: Bearer trk_live_YOUR_API_KEY"
GET /api/v1/events?cursor=&since=&limit= →
{
"items": [
{
"id": 101,
"contact_id": 1,
"kind": "link_click",
"ts": "2026-09-10T09:06:12.000Z",
"verdict": "human",
"classifier_version": 1,
"subject": { "type": "link", "id": 4, "external_ref": "l-4" }
}
],
"next_cursor": "101"
}next_cursor is an opaque, always-increasing id — store it and pass it back as ?cursor= on the next poll. It is not a timestamp and must not be treated as one: events can insert slightly out of time order, so polling by time can permanently miss a row. ?since= is a convenience filter only, never a substitute for the cursor. contact_id is null for an open link's clicks, which belong to nobody in particular.
Understanding the numbers
Every open and every click is judged, and the judgement is returned by name — never folded into a single yes/no. There is no chart or count anywhere in this product with only two buckets, “human” and “not human”, because that would hide which of four different things “not human” actually was.
| Verdict | On screen | What produces it |
|---|---|---|
| human | looks like a person | Nothing flagged it as a machine, and where a send time is known it was not implausibly fast. |
| proxy_open | Gmail or Apple fetched it automatically for the reader | The fetch came from Google's or Apple Private Relay's network. |
| prefetch | a browser or mail app fetched it ahead of time | A prefetch header was present. |
| scanner | a company's security scanner checked it | A HEAD request, a known scanner network, browser navigation headers that were present and wrong, or a fetch within 3 seconds of send that did not look like a browser. |
| unknown | not enough information to say | Usually: no send time recorded yet. |
opens is every fetch of the pixel; opens_human counts only the fetches judged human — and it counts events, not people: the same person opening twice counts twice. The gap between the two is never computed by subtraction — each of the other four verdicts is counted and returned by name under verdicts, so nothing is hidden inside “the rest”. The same five words, and the same rule, apply to link clicks: clicks and clicks_human.
A proxy_open — an email service fetching the image automatically — is not a person reading your email, but it usually means the email was delivered. For a reader whose provider proxies images this way, later opens by the same person often never reach this product at all, once the provider's own cache already holds the image — so opens under-counts for those readers rather than over-counting. This is the one place these numbers are too low rather than too high, and it is worth saying plainly: it is what makes the rest of them credible.
unknown — not enough information to say — usually means no send time has been recorded yet. See POST /api/v1/messages/:id/sent above.
Opting someone out
Called from an unsubscribe or opt-out page you control — not from your backend, and not with an API key. It takes the visitor token carried by a tracked link or the site snippet.
curl -X POST https://t.aurelia.lt/optout \
-H "Content-Type: application/json" \
-d '{"token": "the visitor token from a tracked link or the snippet"}'POST /optout →
{ "ok": true }Always the same response, whether or not the token matched anything — so this endpoint can never be used to check whether a token is valid. It stops tracking for that person from that point on. It does not erase what has already been recorded; for that, erase the contact (see Contacts above).