# The public HTTP routes and SDK

Use the public tracking host for scripts, browser collection, and authenticated backend events. The four routes below are separate from Tracking’s management REST API. The TypeScript client exposes only backend event ingestion.

Choose the surface and credential for the operation:

| Surface | Where | Authenticated with | Documented |
|---|---|---|---|
| **The public edge routes** — the four below | `https://<tracking host>/<product id>/tracking/…` | Nothing, except the Events API, which takes a public access key | This page |
| **The management REST API** — the toolkit's actions: domains, the event catalog, profiles, reports, the Google Ads surfaces | `https://api.lepsto.com` | A platform credential — a session token or an `lsk_` API key as `Authorization: Bearer`; see [API and keys](/interfaces/api-and-keys) | The generated [Tracking API reference](/reference/openapi/tracking) |
| **The pages' own traffic** | Behind the product login | A signed-in session | Not documented as a customer API, because it is not one |

Management actions are callable through MCP and the management API even though they do not live on the public tracking host. [Tracking workflows](/acquire/tracking/workflows) also explains which actions can be workflow steps.

## Three ways to drive Tracking from code

These interfaces operate on the same product’s data, with different coverage:

| Way | What it covers | When to use it |
|---|---|---|
| **MCP** | Every action Tracking offers — listing and verifying domains, managing event names, reading profiles, running the reports, driving the Google Ads integration | The widest surface: the configuration these pages describe screen by screen |
| **REST** | Two surfaces: the four public-edge routes on this page, of which the Events API is the one your backend calls, and the management API on `https://api.lepsto.com` covering the toolkit's actions | Anything that can make an HTTP request; no client library needed |
| **The TypeScript SDK** | One operation — `events.ingest`, the Events API below | Typed backend code sending events |

Tracking tool names follow `tracking_<resource>_<action>`, such as [`tracking_link-domains_list`](/reference/mcp-tools/tracking_link-domains_list) and [`tracking_reports_event_volume`](/reference/mcp-tools/tracking_reports_event_volume). The [MCP catalog](/reference/mcp-tools) is the full reference; individual guides link the tools relevant to their tasks.

### The TypeScript SDK

The `@lepsto/sdk` package requires **Node.js 20 or newer**. It has no runtime dependencies and provides ES module and CommonJS builds, both with types.

```bash
npm install @lepsto/sdk
```

Import Tracking from `@lepsto/sdk/tracking` to avoid loading the other clients. This Acme example uses placeholders: replace `prod_…`, the event id, and Ada’s address, and set `LEPSTO_PUBLIC_KEY` in your backend environment.

```ts

const tracking = createTracking({
  productId: 'prod_…',
  credential: { kind: 'bearer', value: process.env.LEPSTO_PUBLIC_KEY },
});

const { accepted, duplicates, rejected } = await tracking.events.ingest({
  events: [{ event_id: '…', name: 'signup', email: 'ada@acme.com' }],
});
```

Pass `productId`, a `credential`, and optionally `baseUrl` to `createTracking`. For the Events API, use a product public access key with `kind: 'bearer'`, which sends `Authorization: Bearer`. See [Backend access keys](/acquire/tracking/public-keys).

**The Tracking client exposes only `events.ingest`.** It sends the Events API body described below and returns `accepted`, `duplicates`, and `rejected`. Use MCP or the management HTTP API for reports, domains, profiles, and Google Ads actions.

#### Credentials

The SDK maps credential kinds to headers explicitly:

| Credential | Header |
|---|---|
| `publishableKey` | `x-publishable-key` |
| `apiKey` (server only) | `x-api-key` |
| `accessToken` | `authorization: Bearer` **and** `x-access-token` |
| `bearer` | `authorization: Bearer` |

`publishableKey` never uses `x-api-key`: that header identifies a backend caller and bypasses the per-product origin allowlist. Browser credentials must not bypass that check. An `accessToken` uses both listed headers because the public edge strips `authorization` on public routes while preserving `x-access-token`.

For Tracking’s Events API, use the product [public access key](/acquire/tracking/public-keys) with the bearer credential.

#### Retries and the base address

**Retries default to off.** Enable them per call or client only when appropriate for the operation. Errors expose `Retry-After` whether or not retries are enabled. Event ids protect ingestion from duplicates, but a `402` requires the plan or billing refusal to be resolved rather than repeated requests.

Set `baseUrl` to use a different edge, such as a staging environment or local development stack.

## The base address

The public tracking routes use this base path:

```text
https://<tracking host>/<product id>/tracking/<route>
```

- **`<tracking host>`** is the shared Lepsto edge, `public.lepsto.com`, by default, or the host you claimed on the `Tracking domain` page. See [register the domains you track](/acquire/tracking/domains).
- **`<product id>`** is your product's id. It is part of the path, and it is what Tracking reads to know whose data this is.

Copy completed URLs from `Install` for the script and `Public access` for backend events. In examples, replace `<tracking host>` and `<product id>` with those product values.

The three unauthenticated routes accept any origin and use no cookies or credentials.

All four routes are rate limited at the edge to **10,000 requests per minute per IP address**. Batch backend events rather than making one request per event; the browser script already batches its traffic.

## `GET /script/lt.v1.js`

Returns the main browser script.

- **Authentication** — none.
- **Request** — no body. Bodies above 1 KB are refused.
- **Response** — `200` with the script, as `application/javascript; charset=utf-8`, an `ETag`, and `Cache-Control: public, max-age=600, must-revalidate`. A request carrying a matching `If-None-Match` gets `304`.

The filename fixes the script version used by your snippet. Updates within that version arrive through the ten-minute cache without a snippet change; a future version uses a new filename.

## `GET /script/lt-xdomain.v1.js`

Returns the cross-domain code. The main script fetches it when a linking group requires it; do not add a separate tag. Authentication, body limit, and caching match the main script route.

## `POST /collect`

Receives browser batches from the tracking script. In browser code, call `lt(…)` rather than constructing requests to this route; see [Record events](/acquire/tracking/events).

- **Authentication** — none. The product comes from the path.
- **Content type** — anything. The script sends `text/plain; charset=UTF-8` because that avoids a preflight request; `application/json` works too. The body is JSON either way.
- **Body cap** — 64 KiB.

```json
{
  "sent_at": "2026-08-02T09:15:00.000Z",
  "events": [
    {
      "name": "page_view",
      "ts": "2026-08-02T09:14:58.000Z",
      "anonymous_id": "…",
      "session_id": "…",
      "url": "https://acme.com/pricing?utm_source=newsletter",
      "referrer": "https://news.example.com/",
      "title": "Pricing",
      "props": { "plan": "pro" },
      "context": { "screen": "1512x982", "lang": "en-GB", "tz": "Europe/Berlin" },
      "consent_statistics": true,
      "consent_marketing": false
    }
  ]
}
```

Only `name` is required per event. Identification events can also carry `email`, `phone`, and `traits`. Campaign tags and ad click ids are derived from `url`, rather than sent as separate fields. Replace example timestamps, ids, URLs, and properties when adapting this payload.

| Status | When |
|---|---|
| `204` | Empty response; some or all submitted events may have been discarded without storage. |
| `400` | The body is not valid JSON. |
| `413` | The body is larger than 64 KiB. |

An oversized body is stopped at the edge before Tracking receives it. Its `413` response contains JSON, unlike the collector’s empty responses:

```json
{
  "error": {
    "code": "body_too_large",
    "message": "Payload Too Large"
  }
}
```

A custom caller must handle that JSON error body even if it normally expects an empty collector response.

The collector can discard an invalid event while retaining valid events from the batch. An unrecognized product also receives `204` with the batch discarded. **A `204` does not confirm persistence.** When a trial allowance is exhausted or billing prevents storage, `/collect` still returns `204`; those events are lost and cannot be resent from the browser. Check `Events not stored this month` in `Dashboard`, `Live`, or `Reports`, then [troubleshoot missing events](/acquire/tracking/troubleshooting).

## `POST /events`

Accepts named events from your backend. Unlike the script and collector routes, it requires a public access key.

- **Authentication** — a public access key, as a bearer token. The key is created by a product Owner or Admin under `Product` → `Settings` → `Public Access`; see [send events from your backend](/acquire/tracking/public-keys). It is checked at the edge and removed from the request before Tracking sees it.
- **Body cap** — 64 KiB, and at most 500 events in one request.

```http
POST https://<tracking host>/<product id>/tracking/events
Authorization: Bearer YOUR_KEY_HERE
Content-Type: application/json
```

Existing integrations can also send the same key in `X-Api-Key`. `Authorization: Bearer` takes priority if both are present. [Public access](/products/public-access) lists all supported key headers. Replace `YOUR_KEY_HERE` with a backend-held key; never expose it in browser code.

```json
{
  "events": [
    {
      "event_id": "inv_20260802_8891",
      "name": "payment",
      "email": "ada@example.com",
      "amount_minor": 4900,
      "currency": "EUR",
      "ts": "2026-08-02T09:15:00Z",
      "props": { "subscription_ref": "sub_5512" }
    }
  ]
}
```

Names are free-form. `signup`, `trial_started`, `subscription_started`, `payment`, `refund`, and `churn` have lifecycle meaning; `payment` and `refund` require an amount. Other names require only `event_id` and `name`.

Any event can include `amount_minor`, with `currency` required alongside it. Amounts outside the six lifecycle names are retained but do not contribute to revenue. See [Revenue fields](/acquire/tracking/revenue) for units, currency rules, and timestamp limits.

`200` returns batch accounting directly, without an envelope:

```json
{
  "accepted": 1,
  "duplicates": 0,
  "rejected": []
}
```

| Field | Meaning |
|---|---|
| `accepted` | Events stored by this request. |
| `duplicates` | Events whose `event_id` had already been stored for your product. They are not stored twice, which is what makes a retry after a timeout safe. |
| `rejected` | One entry per event that failed validation, with its `index` in your batch, its `event_id` if you sent one, and a `reason`. |

**Inspect `rejected` even after `200`.** A partially valid batch stores valid entries and reports the invalid ones individually:

```json
{
  "accepted": 1,
  "duplicates": 0,
  "rejected": [
    {
      "index": 1,
      "event_id": "inv_20260802_8892",
      "reason": "amount_minor is required for payment"
    }
  ]
}
```

### Errors

Use the status and response shape to distinguish key, body, and entitlement failures.

Key checks run before Tracking receives the request. A failed check returns the edge envelope:

```json
{
  "error": {
    "code": "public_key_invalid",
    "message": "Unauthorized",
    "step": "public_key_check"
  }
}
```

| Status | `code` | Meaning |
|---|---|---|
| `401` | `public_key_required` | No key reached the edge on any accepted carrier. The examples here use `Authorization: Bearer`, and the legacy `X-Api-Key` also carries it; [public access](/products/public-access) lists every accepted header. |
| `401` | `public_key_invalid` | Not one of this product's keys. A revoked key, and a key belonging to another product, both read this way. |
| `403` | `public_key_scope` | A real key whose scope does not cover this route. |
| `413` | `body_too_large` | The body is over 64 KiB. Send fewer events per request. |

The edge also refuses bodies above 64 KiB. That error includes `code` and `message`, but no `step`.

Tracking’s request-validation errors use this flat `message`, `error`, and `statusCode` format. Entitlement refusals use the separate `402` format below.

```json
{
  "message": "request body must be { \"events\": [ ... ] }",
  "error": "Bad Request",
  "statusCode": 400
}
```

| Status | Meaning | What to do |
|---|---|---|
| `400` | The body is not `{ "events": [ … ] }`, or the batch is over 500 events | Fix the request body; split the batch. |
| `402` | Storage is refused for the current plan or billing state; the entire batch is refused | Stop retrying and resolve the `reason` below. |

A `402` from the public-key Events API has this body:

```json
{
  "error": "entitlement_exhausted",
  "reason": "trial_cap_reached"
}
```

`error` is `entitlement_exhausted`. Only the following reasons are documented here; treat other fields as undocumented. This public-key response does not disclose organization plan or usage figures.

| `reason` | Recovery |
|---|---|
| `trial_cap_reached` | Open the organization’s `Billing` page in the Lepsto Workspace. Upgrade from the trial, or wait for the next calendar month. |
| `billing_paused` | Resolve the billing pause on the organization’s `Billing` page, for example a failed payment. |
| `budget_paused` | Raise or remove the reached budget and resume usage on `Billing`. |

A batch that would exceed the remaining allowance is refused whole: nothing is stored, and there is no partial-acceptance `rejected` list. A smaller batch that fits the remaining allowance can be accepted. Do not repeatedly send the refused request while its condition remains. Keep backend events if you intend to resend after resolving it, reuse their `event_id` values, and respect the writable timestamp window. Browser events discarded by `/collect` cannot be recovered this way.

The SDK reports this refusal as `err.code === 'entitlement_exhausted'`:

```ts
try {
  await tracking.events.ingest({ events });
} catch (err) {
  if ((err as { code?: string }).code === 'entitlement_exhausted') {
    // Stop sending; retain events until the refusal is resolved.
  } else {
    throw err;
  }
}
```

A `5xx` means the batch was not stored. Retry the whole batch with unchanged `event_id` values so a repeated request cannot store duplicates.

## What is not here

- **The management API is not on this page** — it is a different host and a different credential, and the generated [Tracking API reference](/reference/openapi/tracking) documents its routes. It is supported; it is simply not part of the public edge.
- **The pages' own traffic is not an API.** The toolkit's screens talk to routes that require a signed-in session on your product. Those are the user interface's own calls, not a customer surface, and they are not documented as one.
- **The tracking script needs no key and must never be given one.** See [send events from your backend](/acquire/tracking/public-keys).

## Next steps

- [Send events from your backend](/acquire/tracking/public-keys): the key this route needs, and replacing it without a gap.
- [Send revenue from your backend](/acquire/tracking/revenue): the fields of a revenue event.
- [Record your own events](/acquire/tracking/events): the browser call these routes sit under.
- [Tracking settings reference](/acquire/tracking/settings): every setting, default and limit in one table.
