Skip to content

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:

WebhooksThe event feed
HowOwnrecord POSTs each event to your endpointYou read GET /events (or the MCP resource ownrecord://events) when you're ready
Which eventsThe brand's commerce eventsEvery event of your scope: organization, legal entity and brand events, compliance events included
OrderNot guaranteed; retries can arrive lateThe order events were recorded; your cursor never skips or repeats one
NeedsA public HTTPS URLNothing 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 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).

EventWhen
registration.state_changedA registration changed state: a threshold crossed, grace started, the deadline passed, you marked it registered…
registration.deadline_escalatedA grace deadline is getting close: half or 80% of the time has passed, or daily in the last three days
threshold.crossedA sale took the entity over a regime's registration threshold
filing_period.readyA filing period is over and its return isn't filed yet: lock it and prepare the report
filing_period.due_soonA return is due in 14, 7 or 2 days and isn't filed yet
rate.changedA 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 was raised, changed, completed or became overdue
approval.requested, approval.resolvedA call needed approval, 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:

Shell
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:

TypeScript
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} returns one event under the same rules.

Events over MCP

AI agents connected to the MCP server 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.