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

# Wire protocol

> What a beacon client sends, what a server must answer, and the status codes that decide whether a client retries, pauses or stops.

Every beacon client speaks this: the JavaScript SDK, the PHP client, the WordPress plugin. Any server that implements it can receive their traffic.

This page is the working summary. The normative document is [PROTOCOL.md](https://github.com/Snow-SEO/beacon/blob/main/packages/beacon-server/PROTOCOL.md) in the repository.

***

## The request

```
POST <endpoint>
X-Beacon-Key: <key>
Content-Type: application/json
```

```json theme={null}
{
  "host": "example.com",
  "hits": [
    {
      "path": "/pricing",
      "userAgent": "Mozilla/5.0 ... compatible; GPTBot/1.2; +https://openai.com/gptbot",
      "ip": "20.171.207.9",
      "format": "html",
      "statusCode": 200,
      "occurredAt": "2026-07-30T10:00:00.000Z"
    }
  ]
}
```

One to 500 hits per request. `path` and `userAgent` are the only required fields on a hit; a hit missing either is dropped and counted, and **the rest of the batch still lands**. A server must never reject a whole batch over one bad item, because clients are heterogeneous enough that one always will be.

The key travels in `X-Beacon-Key` rather than `Authorization`: it identifies a site, not a user, and it has to survive proxies that strip auth headers.

### Fields a server should know about

| Field                                           | Why it matters                                                                                                                             |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `ip`                                            | The only unfakeable part. Without it every hit is `unverified`, and Markdown clients cannot be attributed to a provider at all.            |
| `askedForMarkdown`                              | The strongest signal of an AI client. Claude Code arrives as `axios/1.8.4` and matches no registry token.                                  |
| `headers`                                       | A lowercase-keyed subset of raw request headers. Lets classification improve without every install upgrading first. Tolerate unknown keys. |
| `signature`, `signatureInput`, `signatureAgent` | RFC 9421 headers, verbatim. Proof of identity rather than a claim.                                                                         |
| `rawPath`, `method`                             | Needed only to rebuild a signature base.                                                                                                   |

Clients send raw facts. They do not label a hit as a crawler, name a provider, or claim a verification state.

***

## The response

```json theme={null}
{
  "accepted": 2,
  "skipped": 1,
  "reasons": { "malformed": 0, "unrecognized": 1 }
}
```

`accepted + skipped` always equals `hits.length`. `unrecognized` is normal traffic that is not an AI client, and it is usually the largest number.

***

## Status codes

This is the part that bites. Clients branch on the status, and getting one wrong silences a site.

| Status | Code                      | What the client does                             |
| ------ | ------------------------- | ------------------------------------------------ |
| 401    | `UNAUTHORIZED`            | **Stops for good** and forgets the key.          |
| 403    | `BEACON_HOST_NOT_ALLOWED` | Surfaces the error, keeps sending.               |
| 403    | anything else             | **Pauses for an hour**, keeps recording locally. |
| 400    | `BAD_REQUEST`             | Drops the batch as poison. Never retried.        |
| 413    | `PAYLOAD_TOO_LARGE`       | Drops the batch as poison. Never retried.        |
| 429    | `RATE_LIMITED`            | Honours `Retry-After`, or waits 60 seconds.      |
| 5xx    | `INTERNAL_ERROR`          | Backs off exponentially, capped at an hour.      |

<Warning>
  **Never return a bare 403.** A client reads any 403 that is not `BEACON_HOST_NOT_ALLOWED` as "this account is not entitled" and stops sending for an hour. If you are writing a collector, use 403 for host rejection and nothing else.
</Warning>

<Warning>
  **Always send a status for an oversized body**, rather than closing the connection. A client that sees a socket error backs off and retries a payload it can never deliver.
</Warning>

***

## Timestamps

`occurredAt` lets a client buffer and send later, which every client does.

A server must ignore a timestamp more than 24 hours from its own clock and substitute the receive time. A skewed client clock would otherwise land hits in the wrong day bucket permanently, and there is no way to notice after the fact.

***

## Verification states

| State               | Meaning                                                                                        |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `signed`            | A valid RFC 9421 signature. Proves key possession. Strongest.                                  |
| `verified`          | The address is in the claimed provider's published range, or reverse DNS forward-confirmed it. |
| `unverified`        | Nothing to check against. Not suspicion.                                                       |
| `spoofed_suspected` | Something checkable contradicts the claim.                                                     |

A valid signature outranks the address verdict. A signature that **fails** falls through to the address verdict rather than condemning the hit: the signature base is rebuilt from a forwarded payload, so a gap in your reconstruction must never be recorded as somebody else's forgery.

***

## Things that are deliberately not solved

**Hits are not idempotent.** A client that retries after a timeout may double-count. Buffers are small and the data is analytical, so this is accepted rather than papered over with an idempotency key.

**There is no read API in the protocol.** Ingest is the contract. What a server exposes for reading is its own business.
