---
title: "Server events"
description: "Record backend-confirmed actions with visitor identity and safe delivery retries."
documentation: "https://tracwell.app/docs/server-events"
markdown: "https://tracwell.app/docs/server-events.md"
---

# Server events

> Record backend-confirmed actions with visitor identity and safe delivery retries.

## Choose the right event

| Event | When to record it |
| --- | --- |
| checkout_clicked | Browser observes the checkout link click. |
| begin_checkout | Backend successfully creates the provider checkout session, before redirecting. It does not confirm that the hosted payment page loaded. |
| Payment successful | Verify the payment webhook and send a purchase through the [revenue endpoint](https://tracwell.app/docs/revenue.md). |

Custom names are conventions chosen by your application, not automatic events. Use separate names for the click and confirmed outcome.

## Authentication and identity

POST https://collect.tracwell.app/v1/server/events

Use Authorization: Bearer YOUR_SERVER_KEY and Content-Type: application/json. Create a key in Settings → Website → Server key for an active Product-mode website. This is the same write-only key used for revenue. Keep it server-only. Public project keys and MCP read keys cannot authenticate this endpoint. Project scope comes from the key; omit project IDs and public keys from the body.

Read analytics.getSession() in the browser and pass its anonymousId and sessionId with the business request. Both are required. Preserve them in checkout metadata for revenue attribution. Optional user_id does not replace either. Handle consent and tracking preferences before forwarding identity. Without browser identity, skip the attributed event rather than inventing a journey. A server key authenticates the sender, not the truth of the business action.

## Request

Run this after the business action succeeds. Use the originating page context, not your server URL or device details. No browser SDK runs on the server.

```ts
// Server only. Pass the browser's anonymousId and sessionId
// with the business request, after handling consent on your site.
const batch = {
  batch_version: 1,
  batch_id: crypto.randomUUID(),
  sent_at: new Date().toISOString(),
  sdk: { name: "tracwell-server", version: "0.1.0" },
  events: [{
    schema_version: 1,
    event_id: crypto.randomUUID(),
    event_type: "event",
    event_name: "begin_checkout",
    timestamp: new Date().toISOString(),
    anonymous_id: anonymousId,
    session_id: sessionId,
    context: {
      url: "https://example.com/pricing",
      path: "/pricing"
    },
    properties: { plan: "pro", provider: "dodo" }
  }]
};

// Create this batch once per action. Retry this same serialized body
// and event_id; persist it if retries must survive a process restart.
const body = JSON.stringify(batch);
const response = await fetch(
  "https://collect.tracwell.app/v1/server/events",
  {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.TRACWELL_SERVER_KEY}`,
      "content-type": "application/json"
    },
    body,
    signal: AbortSignal.timeout(5000)
  }
);
const result = await response.json();
if (response.status !== 202 || result.receipt_version !== 1 ||
    result.batch_id !== batch.batch_id ||
    result.accepted_events !== batch.events.length) {
  throw new Error("Tracwell collection was not acknowledged");
}
```

| Field or limit | Requirement |
| --- | --- |
| batch_version | 1 |
| batch_id | UUID |
| sdk | name: tracwell-server; version: semantic version such as 0.1.0 |
| events | 1–50 custom events; batch at most 48 KiB |
| Event fields | schema_version: 1, event_id: UUID, event_type: event, event_name, timestamp, anonymous_id, session_id, context |
| Context | Required absolute url and slash-prefixed path; optional title, referrer, utm_source, utm_medium, utm_campaign, utm_term, utm_content |
| Timestamps | ISO with milliseconds; no more than 24 hours old or 5 minutes ahead, including sent_at |
| Event size | At most 16 KiB |
| Reserved | identify, page_view and revenue names; properties beginning with $revenue_ |

Optional user_id must be opaque. Properties and names follow [custom event limits](https://tracwell.app/docs/events.md). Duplicate IDs within a batch are rejected. Purchases and refunds belong on the revenue endpoint.

## Acceptance and retries

```json
{
  "receipt_version": 1,
  "receipt_id": "0190f3c6-7a10-7cc2-8b48-8e1acb5f8c01",
  "batch_id": "0190f3c6-7a10-7cc2-8b48-8e1acb5f8b01",
  "accepted_events": 1,
  "accepted_at": "2026-09-05T12:00:00.000Z"
}
```

A matching 202 receipt confirms Queue acceptance, not database persistence. Reuse the exact original IDs, timestamps and payload when retrying; the ingestion/reporting path deduplicates repeated delivery. Await delivery before returning from a short-lived server function. Use bounded backoff for network errors and retryable responses. Retry across process restarts requires durable storage in your application; in-memory retries cannot guarantee it.

For checkout, report analytics failures separately and preserve the valid checkout redirect. Verify the resulting event after ingestion before declaring the integration complete.

| HTTP status | Action |
| --- | --- |
| 400 | Invalid JSON, schema, timestamp, event size, or reserved revenue fields. Correct the payload. |
| 401 | Missing or unavailable server key, including a website without active Product-mode collection access. |
| 405 / 415 | Use POST with Content-Type: application/json. |
| 413 | Request exceeds the byte limit. Reduce the batch size. |
| 429 | Read error.retryable: retryable rate limits may be retried; an exhausted event allowance is not retryable. |
| 500 / 503 | Temporary collection failure. Retry the same batch with bounded backoff. |
