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

# Events and operations

> List and poll event history, stream events in real time, replay events, and wait for asynchronous operations.

Every webhook event is also stored in event history. Read it to backfill,
reconcile, or receive events without a public endpoint. Operations track work
that finishes after the request returns, such as a campaign send.

| Task | Permission |
| - | - |
| List and read events | `events:read` |
| Replay an event to a webhook | `events:replay` |
| Stream events (beta) | `events:listen` |
| Read and wait for operations | `operations:read` |
| Cancel an operation | `operations:cancel` |

A project client sees its project. A team client sees team events and every
project's events.

## List events

```typescript theme={null}
for await (const event of await project.events.list({ type: "message.received", limit: 100 })) {
  console.log(event.id, event.type, event.createdAt, event.payloadAvailability);
}
```

Filter with `type`, `since`, and `until`. To read one event with its body, ask
for the payload:

```typescript theme={null}
const event = await project.events.retrieve("<event-id>", { includePayload: true });

if (event.data.payload) {
  const body = JSON.parse(atob(event.data.payload.data));
  console.log(body);
}
```

The body is base64-encoded JSON. `payloadAvailability` tells you when a body is
not available, for example after it expired. See [Event history](/api/events).

## Poll for new events

`listIndexed` returns events in the order Polymorfa stored them, not the order
they happened. Use it for polling: an event that is stored late, such as a
message received after a reconnect, still appears after your saved offset.
Start from a baseline, then poll from the saved offset:

```typescript theme={null}
// Baseline: start after everything stored so far.
let offset = (await project.events.listIndexed({ afterOffset: "0", limit: 1 })).page.highWatermark;

async function poll() {
  const result = await project.events.listIndexed({ afterOffset: offset, limit: 100 });
  for (const event of result.items) console.log(event.id, event.type);

  offset = result.page.hasMore && result.page.nextOffset
    ? result.page.nextOffset
    : result.page.highWatermark;
  return result.page.hasMore;
}

while (await poll()) {}
```

While `hasMore` is `true`, continue from `nextOffset`. When the page is
exhausted, save `highWatermark` and pass it on your next poll. Offsets are
decimal strings. Keep them as strings.

## Stream events in real time

<Note>
  Event streams are a beta. A team owner or admin enrolls the team in the Console.
  Without enrollment, the stream throws `PolymorfaAuthorizationError` with code
  `feature_unavailable`. See [Event streams](/api/event-streams).
</Note>

```typescript theme={null}
const controller = new AbortController();
process.once("SIGINT", () => controller.abort());

for await (const item of project.events.stream({
  types: ["message.*", "session.status"],
  signal: controller.signal,
})) {
  console.log(item.event.id, item.event.type, item.webhook?.payload);
  // Save item.cursor after you process the event.
}
```

`stream()` reconnects after a dropped connection and resumes from the last
event it delivered. Each item has:

* `event`: the event record, with the same fields as `events.retrieve`.
* `webhook`: the webhook body, or `null` when the body was not kept.
* `cursor`: the position after this event.

To resume after a restart, pass your saved cursor as `since`. Pass `onGap` to
learn when events expired before you resumed. Aborting the signal or leaving the
loop closes the stream. A team client must pass `projectId`.

Run streams on your server only. Client tokens cannot open them.

## Replay an event

Send a stored event to one webhook endpoint again:

```typescript theme={null}
const replay = await project.events.replay(
  "<event-id>",
  { webhookId: "<webhook-id>" },
  { idempotencyKey: crypto.randomUUID() },
);

console.log(replay.data.deliveryId, replay.data.operationId);
```

Only the endpoint you name receives the event. An event can be replayed until
its `replayableUntil` time.

## Wait for an operation

Methods that start background work return an operation ID. Wait for it to
finish:

```typescript theme={null}
const operation = await platform.operations.wait("<operation-id>", {
  maxWaitMs: 10 * 60_000,
});

if (operation.data.status !== "succeeded") {
  console.error(operation.data.status, operation.data.error?.code);
}
```

`wait` returns when the operation succeeds, fails, or is cancelled, or when
`maxWaitMs` ends (5 minutes by default). It returns the latest state either
way, so always check `status`.

## Find and cancel operations

```typescript theme={null}
for await (const running of await platform.operations.list({ status: "running" })) {
  console.log(running.id, running.kind, running.status);

  if (running.capabilities.cancellable) {
    await platform.operations.cancel(running.id);
  }
}
```

`get(id)` reads one operation. Pass `{ wait: 30 }` to hold the request open for
up to 30 seconds until the operation changes. `listTransitions(id)` lists its
state changes. Cancelling works only while `capabilities.cancellable` is
`true`. See [Operations](/api/operations).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.