> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trevosdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# REST

> Record conversions over plain HTTP from any language.

For languages Trevo does not ship an SDK for — Go, Python, Ruby, Elixir, PHP, Java, Dart.
Tracking is a plain HTTP POST with no client-side logic, so there is nothing to get subtly
wrong.

## Do not implement assignment yourself

Assignment is a hash whose exact behaviour is a frozen contract shared by every Trevo SDK:
FNV-1a over **UTF-16 code units**, not UTF-8 bytes.

An implementation written against bytes agrees with ours on ASCII identifiers and diverges
on every accented name, every CJK id, every emoji. It passes your tests and silently
corrupts that customer's experiment months later. Nothing errors — the numbers just stop
meaning anything.

If you only need to record conversions, everything below is safe and sufficient: resolve
variants in the browser or a Node service, and use REST purely for tracking.

If you genuinely need assignment in an unsupported language, implement the
[bucketing spec](/reference/bucketing-spec) **and** pass every case in the published
conformance vectors before sending a single exposure.

## Authentication

A server key (`tsk_secret_…`) from Settings → API keys, platform `Server`:

```
Authorization: Bearer tsk_secret_…
```

Publishable `tsk_live_` keys are for browsers. A secret key sent with an `Origin` header is
rejected, because that combination means it has been shipped into client code.

## Send events

```
POST https://ingest.trevosdk.com/v1/events
Content-Type: application/json
```

```json theme={null}
{
  "events": [
    {
      "event": "subscription_started",
      "userId": "user_123",
      "anonymousId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
      "properties": { "plan": "pro", "value": 49.99 },
      "timestamp": "2026-08-09T12:34:56.000Z"
    }
  ]
}
```

| Field         | Required       | Notes                                    |
| ------------- | -------------- | ---------------------------------------- |
| `event`       | yes            | ≤500 chars, `A–Z a–z 0–9 _ . - $` only   |
| `userId`      | one of the two | ≤255 chars                               |
| `anonymousId` | one of the two | The `trevo_id` cookie value              |
| `properties`  | no             | JSON object, ≤8192 bytes serialised      |
| `timestamp`   | no             | ISO 8601. Defaults to receipt time       |
| `insertId`    | no             | Idempotency key, ≤200 chars              |
| `sdkVersion`  | no             | Free-form, useful for your own debugging |

Up to **500 events per request** — batch rather than sending one request per event. The
body also accepts a top-level `sentAt` (ISO 8601), set when the batch leaves your client.

### Send both ids

Trevo links an anonymous journey to a user the first time it sees an event carrying both. A
backend that only ever sends `userId` never creates that link, and the user's pre-login
browsing is never attached to their conversion.

## Timestamps and wrong clocks

Events are **never dropped for their timestamp.**

`receivedAt - sentAt` gives your client's clock offset, and every event in the batch is
corrected by it. That distinguishes "this clock is wrong" from "this event is genuinely
old", which comparing against the server clock alone cannot do.

* Offsets under 2 seconds are treated as network transit and ignored
* Corrections are capped at ±24 hours
* After correction, timestamps older than 7 days or more than 1 hour in the future are
  clamped to the boundary and marked, not discarded

Send `sentAt` if you buffer events. Without it, timestamps are taken at face value and only
the sanity clamp applies — fine for a server sending events as they happen, wrong for
anything that queues.

## Link identities explicitly

When the caller has no cookie to read — a webhook, a cron job — state the link directly:

```
POST https://ingest.trevosdk.com/v1/alias
Authorization: Bearer tsk_secret_…
```

```json theme={null}
{ "anonymousId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301", "userId": "user_123" }
```

Returns `202` with an outcome of `linked` or `already-linked`. Sending the same pair
repeatedly is a no-op, so it is safe inside a handler your provider may replay.

An `anonymousId` belongs to one user. Pointing one at a *different* user returns `409` and
changes nothing — that request is usually a shared browser or a reused cookie, and honouring
it would credit one person's browsing to another.

## Idempotency

Set `insertId` to a stable value derived from the thing that happened — an order id, a
subscription id, `${userId}:${invoiceId}` — and Trevo records that event once no matter how
many times you send it.

This matters most where you least want a duplicate: payment providers replay webhooks, and a
`subscription_started` counted twice inflates the conversion rate of whichever variant that
user was in.

**Without `insertId`, retries are not deduplicated.** A request that timed out after the
server accepted it will be counted twice if you send it again.

One boundary: deduplication is scoped to the calendar month the event was recorded in. Two
deliveries of the same `insertId` landing in different months are both kept. Rarely
reachable in practice — but if you replay historical events in bulk, do it within a month's
window.

## Responses and retries

| Status | Meaning                                                | Safe to retry                 |
| ------ | ------------------------------------------------------ | ----------------------------- |
| `202`  | Accepted and queued                                    | —                             |
| `400`  | Validation failed — the body names the offending field | No, not unchanged             |
| `401`  | Missing, malformed, revoked, or wrong-class key        | No                            |
| `409`  | `/alias` only — already linked to another user         | No                            |
| `429`  | Rate limited or daily cap reached                      | Yes, after backing off        |
| `5xx`  | Server error; the batch was not recorded               | Yes, with exponential backoff |

A timeout or dropped connection is the ambiguous case: the request may or may not have
landed. Send `insertId` and retry freely.

## Minimal example

```bash theme={null}
curl -X POST https://ingest.trevosdk.com/v1/events \
  -H "Authorization: Bearer $TREVO_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"event":"subscription_started","userId":"user_123","properties":{"plan":"pro"}}]}'
```
