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

# Beacon protocol

> Exact wire format for /e, /e/validate, /err, /m, /g, and /preview-config. If you build a client, this is what to send.

Every beacon the snippet sends is `text/plain` on purpose, that
lets `sendBeacon` work without a CORS preflight. Payloads are JSON
strings.

Fields marked *optional* are omitted from the body when they have no
meaningful value; do not send them as `null`.

## `POST /e`, exposures and goals

The main beacon. Batched exposures and goal fires from a pageview.

```json theme={null}
{
  "s": "<siteId>",
  "u": "<userId>",
  "v": <configVersion>,
  "t": <clientTimestampMs>,
  "nr": "n" | "r",
  "rf": "<trafficChannel>",
  "uc": "<utmCampaign>",
  "p": { "lcp": <ms>, "a": { "<expId>": <ms> } },
  "e": [
    { "i": "<experimentId>", "k": "<variantKey>", "bn": "l" | "s", "gn": <generation> }
  ],
  "g": [
    {
      "i": "<experimentId>",
      "k": "<variantKey>",
      "gi": "<goalId>",
      "v": <value>,
      "bn": "l" | "s",
      "gn": <generation>,
      "oid": "<orderId>",
      "cur": "<currency>",
      "rt": "refund",
      "rid": "<refundId>",
      "eid": "<eventId>"
    }
  ]
}
```

* `s`, `u`, `v`, `t`, always present. Site id, visitor id, config
  version, client ms since epoch.
* `nr`, `n` for new (within 30-min gap), `r` for returning.
* `rf`, bucketed traffic channel (direct/organic/paid/social).
* `uc`, `utm_campaign` verbatim.
* `p`, LCP in ms plus per-experiment attribution.
* `e[]`, exposure records.
* `g[]`, goal records.

Each item's `bn` is `l` for "bucketed on page load" or `s` for
"bucketed on an SPA route change".

`gn` is the exclusion-group generation counter, if the experiment is
a member of a group.

For goals, `eid` is a UUIDv7 per fire, the ledger uses it for dedup
on retries.

Response: `200 OK` with an empty body.

## `POST /e/validate`, preview verdict

Same payload as `/e`. Server does not persist. Returns:

```json theme={null}
{ "accepted": true, "reason": null }
```

or

```json theme={null}
{ "accepted": false, "reason": "location_not_matched" | "audience_not_matched" | "not_eligible_by_bucket" }
```

The preview modal uses this to explain why an experiment did or did
not fire on the current page.

## `POST /err`, snippet errors

First-occurrence-per-(phase, expId) per session. Deduped server-side.

```json theme={null}
{
  "s": "<siteId>",
  "v": <configVersion>,
  "t": <clientTimestampMs>,
  "errors": [
    {
      "e": "<experimentId>",
      "vk": "<variantKey>",
      "ph": "apply" | "cleanup" | "route-handler" | "variant-js" | "async" | "global-js",
      "m": "<messageString>",
      "st": "<stackString>",
      "u": "<pageUrl>"
    }
  ]
}
```

## `POST /m`, library telemetry

Per-library-load event from the dev libraries dependency manager.
Small, non-per-visitor.

```json theme={null}
{
  "s": "<siteId>",
  "e": "<experimentId>",
  "vk": "<variantKey>",
  "lib": "<libraryKey>",
  "phase": "load-start" | "load-end" | "load-error",
  "t": <clientTimestampMs>,
  "dur": <durationMs>
}
```

## `GET /g`, edge geo

Returns:

```json theme={null}
{ "country": "US", "region": "CA", "city": "San Francisco" }
```

Cached per-session by the snippet. Bounded wait of 250 ms.

## `GET /preview-config?exp=<uuid>`, one experiment for preview

Returns a draft or paused experiment's config blob. CORS `*`. Used by
the preview modal to fetch experiments not yet in the site config.

## CORS

Every visitor-facing beacon is `Access-Control-Allow-Origin: *`.
`sendBeacon` with `text/plain` does not preflight, so wildcards work.

## Auth

None on the visitor-facing endpoints. The snippet is public; anyone
can read a config for a site they know the id of. That is deliberate
clients cannot secure themselves.

Dashboard endpoints under `/api/*` require a Clerk bearer token or an
API key. Rate-limited at 100 requests per minute per authenticated
user.

## If you build your own client

Everything on this page is stable. Method versions in the ledger are
locked per experiment; the wire format follows semver, a breaking
change bumps a version.

If you build a serious integration, tell us. Helps us not break you.
