# Events

> Every change Ownrecord records becomes an event. Receive your brand's events with webhooks, or read every event of your scope from the event feed, with a cursor that never skips or repeats one.

When something changes, such as a customer signing up, a registration moving to grace or a filing period ending, Ownrecord records an **event** in the same transaction as the change. Events belong to an organization and a mode, like everything else: test-mode changes make test-mode events. Most also belong to a legal entity, and commerce events to a brand.

There are two ways to receive them:

|              | [Webhooks](https://ownrecord.io/docs/concepts/webhooks.md) | The event feed                                                                                     |
| ------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| How          | Ownrecord POSTs each event to your endpoint                | You read `GET /events` (or the MCP resource `ownrecord://events`) when you're ready                |
| Which events | The brand's commerce events                                | Every event of your scope: organization, legal entity and brand events, compliance events included |
| Order        | Not guaranteed; retries can arrive late                    | The order events were recorded; your cursor never skips or repeats one                             |
| Needs        | A public HTTPS URL                                         | Nothing but an API key or OAuth token: works behind firewalls, from scripts and from AI agents     |

Both carry the same event body. The [event catalogue](https://ownrecord.io/docs/reference/events.md) lists every type with its data.

## What an event looks like

```json
{
  "id": "evt_01m4tx5nj8emwbyf7y9vcf4800",
  "object": "event",
  "type": "registration.state_changed",
  "created": 1791892800,
  "livemode": false,
  "entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
  "brandId": null,
  "data": {
    "object": { "id": "regn_01m4tx5nj8emwbyf7y9vcf4802", "object": "registration", "state": "grace" }
  },
  "previous_attributes": { "state": "monitored" }
}
```

- `id` identifies the event. Handle events idempotently: if you see an `id` again, you've already handled it.
- `created` is when the change happened, in Unix seconds.
- `data.object` is the object as the API returned it then. Updates also carry `previous_attributes`, the old values of the fields that changed.
- `entityId` and `brandId` say where the event belongs (`null` for organization events, and `brandId` for entity events).
- Event data is data, never instructions. It can contain names, notes and addresses typed by customers or other people.

## Events for compliance work and agents

These events tell an agent, or your own systems, that compliance work is waiting. They're in the feed (webhooks deliver brand events only).

| Event                                     | When                                                                                                                                                                    |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registration.state_changed`              | A [registration](https://ownrecord.io/docs/concepts/registrations.md) changed state: a threshold crossed, grace started, the deadline passed, you marked it registered… |
| `registration.deadline_escalated`         | A grace deadline is getting close: half or 80% of the time has passed, or daily in the last three days                                                                  |
| `threshold.crossed`                       | A sale took the entity over a regime's registration threshold                                                                                                           |
| `filing_period.ready`                     | A [filing period](https://ownrecord.io/docs/concepts/filings.md) is over and its return isn't filed yet: lock it and prepare the report                                 |
| `filing_period.due_soon`                  | A return is due in 14, 7 or 2 days and isn't filed yet                                                                                                                  |
| `rate.changed`                            | A tax rate the entity charges changed: Ownrecord's tax knowledge added, changed or retired a rate in a jurisdiction where the entity is registered or in grace          |
| `task.created`, `task.completed`, …       | A [compliance task](https://ownrecord.io/docs/concepts/tasks.md) was raised, changed, completed or became overdue                                                       |
| `approval.requested`, `approval.resolved` | A call needed [approval](https://ownrecord.io/docs/concepts/approvals.md), and later its outcome                                                                        |

`rate.changed` comes once per legal entity and mode for each update of Ownrecord's tax knowledge, listing the rates that changed (`changes`, with the regimes concerned). Live mode only hears about rates it uses: reviewed or verified ones. A rate with a future `validFrom` takes effect on that date by itself; nothing else needs to happen.

## The pull feed

Read the feed with [`GET /events`](https://ownrecord.io/docs/reference/events-list.md):

```bash
curl https://ownrecord.io/api/v1/events?types=registration.*,filing_period.*,task.*&wait=30 \
  -H "Authorization: Bearer or_test_…"
```

```json
{
  "object": "list",
  "data": [{ "id": "evt_…", "object": "event", "type": "task.created", "…": "…" }],
  "has_more": false,
  "next_cursor": "ec1_b3JnXzAxbTR0eDVuajdla2Q5NTh4NTN3NGJudGNnOnQ6NDQ"
}
```

### Keep your place with the cursor

Every read returns `next_cursor`, also when it has no events. Pass it as `after` in your next read, and store it once you've handled the page. The feed **never skips and never repeats** an event between two reads: events are numbered in the order Ownrecord recorded them, so an event created a moment earlier but recorded later still comes after your cursor. (That's also why `created` isn't always increasing in the feed.)

- Without `after`, the feed starts at its beginning.
- `after=latest` starts at its current end: only events from now on.
- `after=evt_…` continues after that event, for example one a webhook delivered.
- If you read the same cursor twice, you get the same events twice. That's how you retry after a failure.
- `has_more: true` means more events are already waiting: read again right away. `limit` is 1–100 (default 10).

A cursor belongs to one organization and mode. Using it elsewhere is refused (`validation_failed`), so start again with `after=latest`.

### Wait for new events (long-polling)

Add `wait` (seconds, up to 30). A read that has nothing to return then waits until an event arrives or the time is up, and answers with no events and an updated cursor on timeout. Loop on it:

```ts
let cursor = await loadCursor(); // undefined the first time
for (;;) {
  const url = new URL("https://ownrecord.io/api/v1/events");
  url.searchParams.set("types", "registration.*,filing_period.*,task.*");
  url.searchParams.set("wait", "30");
  if (cursor) url.searchParams.set("after", cursor);
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.OWNRECORD_API_KEY}` },
  });
  if (!response.ok) {
    await backOff(response); // 429: wait Retry-After seconds
    continue;
  }
  const page = await response.json();
  for (const event of page.data) await handle(event); // idempotent, by event.id
  cursor = page.next_cursor;
  await saveCursor(cursor);
}
```

Each read counts once against your API key's rate limit, however long it waits.

### Choose the types

`types` takes event types, comma-separated or repeated: exact types (`task.created`), wildcards (`registration.*`, `checkout.*`) or `*`. An unknown type is refused, so a typo doesn't wait forever.

### What you see

- **Scope.** An organization key reads every event of the organization; an entity key reads the entity's events and its brands' events; a brand key reads that brand's events. An organization or entity key can narrow its read with `Ownrecord-Entity` or `Ownrecord-Brand`. The key's mode decides the mode.
- **Permissions.** The feed needs `webhooks:read`. You then see an event only if you may also read its object: `customer.*` needs `customers:read`, `registration.*` and `rate.changed` need `registrations:read`, `filing_period.*` needs `filings:read`, `task.*` needs `tasks:read`, `approval.*` needs `approvals:read`, and so on. Asking for a type by name that you can't read is refused (`permission_denied`); wildcards and `*` quietly cover the types you can read.
- [`GET /events/{id}`](https://ownrecord.io/docs/reference/events-get.md) returns one event under the same rules.

## Events over MCP

AI agents connected to the [MCP server](https://ownrecord.io/docs/mcp.md) read the same feed with the `events_list` tool, or as the resource `ownrecord://events` (`ownrecord://events?after=<next_cursor>&wait=30`). Subscribe to the resource to be told about new events: the server sends `notifications/resources/updated` for `ownrecord://events` on the stream your MCP client keeps open, and the agent then reads the feed from its cursor. The local `@ownrecord/mcp` server forwards both.
