HTTP API
Send product events from your backend. Base URL: https://usetamari.com/api/v1. Every endpoint answers 202 once the data is queued, and it reaches the dashboard within seconds.
Authentication
Send your write key as a bearer token:
Authorization: Bearer <write_key>
Create keys in Settings → Write keys. A key is shown once. Revoked keys answer 401.
POST /api/v1/track
Record a conversion for one of your users, when it happens.
| Field | Required | Notes |
|---|---|---|
type | yes | signed_up, first_api_call, went_live, or custom:<name> (a–z, 0–9, _ . -) |
user_id | at least one identifier | Your own user ID |
email | Hashed and encrypted on arrival | |
github_login | Links the signup to GitHub activity | |
anonymous_id | The snippet's visitor ID, if your frontend passes it to your backend | |
occurred_at | no | ISO 8601 with time zone. Defaults to when Tamari receives it. Up to 5 minutes in the future |
properties | no | Any JSON object |
heard_about | no | The answer to “How did you hear about us?”, up to 255 characters |
message_id | no | Your unique ID for this event. A retry with the same ID is ignored |
signed_up, first_api_call and went_live are recorded once per developer. If one arrives twice, the earliest occurred_at is kept. Custom types are recorded every time.
curl https://usetamari.com/api/v1/track \
-H "Authorization: Bearer $TAMARI_WRITE_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "signed_up",
"user_id": "usr_123",
"email": "ada@example.com",
"heard_about": "Conference talk",
"message_id": "signup-usr_123"
}'POST /api/v1/identify
Link identifiers that belong to the same person, for example when a visitor signs in, so their earlier anonymous page views are credited to them. Each item needs at least two of user_id, email, github_login and anonymous_id. occurred_at is optional.
curl https://usetamari.com/api/v1/identify \
-H "Authorization: Bearer $TAMARI_WRITE_KEY" \
-H "Content-Type: application/json" \
-d '{ "anonymous_id": "a1b2c3", "user_id": "usr_123", "email": "ada@example.com" }'The snippet's window.tamari.identify() calls this for you from the browser.
POST /api/v1/collect
Page views from the browser snippet. You do not need to call it. It accepts text/plain bodies (for navigator.sendBeacon) and takes the key in the body as write_key. Query strings and fragments are removed from the page and referrer addresses before storage.
Batches
/track and /identify accept a JSON array of up to 100 items instead of one object. /collect accepts { "write_key": "…", "batch": [ … ] }. A batch is checked as a whole: one invalid item rejects the request, and nothing is recorded.
Responses
| Status | Meaning |
|---|---|
| 202 | Accepted: { "accepted": <count> }. Processing is asynchronous |
| 400 | Invalid body: { "error", "issues": [{ "path", "message" }] }. In a batch, path starts with the item's index |
| 401 | Missing, unknown or revoked write key |
| 413 | Body larger than 512 KB |
| 429 | Rate limit exceeded. Retry after the Retry-After header, in seconds |
| 500 | Tamari error. Safe to retry with the same message_id |
Rate limits
Each write key can send bursts of up to 1,000 items, and 100 items per second sustained. Each item in a batch counts as one.
Privacy
Identifiers are hashed on arrival with a key unique to your workspace, before they are queued. Email addresses and GitHub usernames are also stored encrypted so the dashboard can show them; it masks email addresses. Raw values never reach Tamari's queue or logs. Do not send sensitive data such as health or financial information in properties.