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

# Event capture API

> Send tracking events without the tag, from a native app or a client you control.

The endpoint behind [`identify()` and `track()`](/mk/developer/web-edge/identify-and-track). Use it directly when the tag isn't an option.

For an ordinary web page, use the tag — it handles the consent gate, batching, buffering and identity for you.

## The request

```
POST https://mk.brand.com/_ag/e
Content-Type: application/json
X-AG-Site-Key: agsk_…
X-AG-Consent: granted        (conditional — see below)
```

```json theme={null}
{
  "events": [
    {
      "event": "Pricing Calculator Used",
      "properties": { "seats": 250, "url": "https://www.brand.com/pricing" }
    },
    {
      "event": "$identify",
      "traits": { "email": "ada@example.com", "company": "Northwind" }
    }
  ]
}
```

| Part            | Value                                                                  |
| --------------- | ---------------------------------------------------------------------- |
| `X-AG-Site-Key` | Your account's tracking key. Required                                  |
| `X-AG-Consent`  | `granted`, when the visitor consented and no identity cookie proves it |
| Body            | `{ "events": [ … ] }`, 1 to **20** events                              |
| Credentials     | Send them if same-site, so the identity cookie rides along             |

## One event

| Field        | Type                | Notes                                                                |
| ------------ | ------------------- | -------------------------------------------------------------------- |
| `event`      | string, 1–200 chars | Required. `$identify` is the reserved name for an identity assertion |
| `properties` | object              | Optional. What happened                                              |
| `traits`     | object              | Optional. About the person. Used by `$identify`                      |

**There's no timestamp field, and one won't be accepted.** allGood records when it received the event; a caller-supplied time would let any site running the tag write arbitrary history. Source and request metadata are stamped by allGood and never read from your body — that's what stops one caller claiming to be another account's traffic.

## The consent header

Under the default posture, allGood requires *evidence* that the visitor consented. It accepts either a valid identity cookie on the request, or `X-AG-Consent: granted`.

On a page allGood doesn't serve there's no cookie to prove it with, and the consent platform lives in the page — so under a blocking posture this header is the only thing that can carry the answer at all.

It's an unverifiable claim, and deliberately so: it's bounded by the checks above it, an allowlisted origin presenting the account's own key. Send it **only** when the visitor's signal really says yes.

If the account has **No tracking before opt-in** off, the header is ignored.

## The order of checks

Deliberately mirrors the [form endpoint](/mk/developer/web-edge/reference/form-api), so the two can't report different outcomes for the same class of problem.

| # | Check                                 | Failure                                      |
| - | ------------------------------------- | -------------------------------------------- |
| 1 | Origin is same-origin or allowlisted  | `unknown_source` · 403 · **no CORS headers** |
| 2 | Tracking enabled, and the key matches | `unknown_source` · 403                       |
| 3 | `Content-Type` is `application/json`  | `validation` · 400                           |
| 4 | Body within the size limit            | `validation` · 400                           |
| 5 | Rate-limit budgets not exhausted      | `rate_limited` · 429                         |
| 6 | Consent evidence present, if required | `consent_missing` · 403                      |
| 7 | Body parses, 1–20 well-formed events  | `validation` · 400                           |
| — | Otherwise                             | `accepted` · 202                             |

A revoked key refuses everything: the record is kept with its fingerprint cleared, so revocation reads as "refuse" rather than "this account never needed a key".

## Responses

```
HTTP/1.1 202 Accepted
{ "status": "accepted", "accepted": 2 }
```

`202`, not `200`: these are accepted for processing, not processed. Nothing downstream is synchronous.

An accepted batch also sets the identity cookie if the request didn't carry a valid one. That's the one place the cookie is minted for an embed — without it, every visitor on your own website would look like a new device on every request.

## Limits

| Limit              | Value                                                  |
| ------------------ | ------------------------------------------------------ |
| Events per request | 20                                                     |
| Payload            | Your account setting (8 KB default), never above 16 KB |
| Per IP             | 30 requests / 60s                                      |
| Per origin         | 300 requests / 60s                                     |

These budgets are **separate** from the form ones, so a flood of tracking can't exhaust the allowance real submissions depend on.

## Identity

| Situation                                   | What happens                                                                          |
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
| The request carries a valid identity cookie | Every event is attributed to that device                                              |
| It doesn't                                  | One identifier is minted for the **whole batch**, and allGood tries to set the cookie |

One identifier per batch rather than per event is deliberate: the events came from a single visitor in a single flush, and giving each its own would invent a crowd.

The cookie belongs to your allGood subdomain and isn't readable by page script. Where your website and your allGood domain share a registrable domain it rides along with capture requests; where they don't, it doesn't, and each request looks like a new device. In that case `$identify` with an `email` is the only thing tying a visitor's events together.

## URLs

A `url` property is treated the same way as [in the tag](/mk/developer/web-edge/identify-and-track): attribution parameters are lifted out, personal-looking query values are removed, and path and host are added.

Because the request goes to your allGood subdomain, allGood can't infer which page the visitor is on. Sending `properties.url` is how you tell it.

## What this endpoint is not

* **Not a webhook receiver.** Third parties can be pointed at it, but they need your tracking key and there's no signature verification.
* **Not a query API.** It's write-only. Read your activity data through allGood's database and warehouse surfaces.
