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

# Events

> Subscribe to inbound messages and status updates with resumable cursors

export const TypeTooltip = ({name, type, children}) => {
  const [visible, setVisible] = React.useState(false);
  const [pos, setPos] = React.useState({
    top: 0,
    left: 0
  });
  const triggerRef = React.useRef(null);
  const show = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPos({
        top: rect.bottom + 6,
        left: rect.left
      });
    }
    setVisible(true);
  };
  const hide = () => setVisible(false);
  return <>
      <span ref={triggerRef} onMouseEnter={show} onMouseLeave={hide} style={{
    cursor: "pointer",
    position: "relative",
    display: "inline"
  }}>
        {children || <code>{name}</code>}
      </span>
      {visible && <span style={{
    position: "fixed",
    top: pos.top,
    left: pos.left,
    zIndex: 9999,
    padding: "8px 12px",
    borderRadius: "8px",
    fontSize: "13px",
    lineHeight: "1.5",
    fontFamily: "'Azeret Mono', monospace",
    whiteSpace: "pre",
    backgroundColor: "var(--tw-prose-pre-bg, #1e1e1e)",
    color: "var(--tw-prose-pre-code, #e5e5e5)",
    border: "1px solid var(--border, rgba(128,128,128,0.2))",
    boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
    pointerEvents: "none"
  }}>
          {type}
        </span>}
    </>;
};

`client.events.subscribe()` returns an async iterable of every event Meta sends for your business account — inbound messages and delivery status updates. The stream reconnects automatically on failure and buffers missed events while you're offline.

## The event stream

```ts theme={null}
for await (const event of client.events.subscribe()) {
  switch (event.type) {
    case "message":
      // inbound message from a user
      break;
    case "status":
      // status update for a message you sent
      break;
  }
}
```

Every event carries a `cursor` string. The cursor advances with each event — save the most recent one and pass it back on restart to pick up where you left off:

```ts theme={null}
for await (const event of client.events.subscribe({ cursor: lastCursor })) {
  lastCursor = event.cursor;
  await persist(lastCursor); // durable storage
  // handle event...
}
```

## Subscribe options

<Accordion title="SubscribeOptions" description="">
  | Option      | Type               | Description                                          |
  | ----------- | ------------------ | ---------------------------------------------------- |
  | `cursor`    | `string`           | Resume from a previously saved cursor.               |
  | `reconnect` | `ReconnectOptions` | Reconnection configuration for automatic reconnects. |
</Accordion>

## Inbound messages

When `event.type === "message"`, the event carries an <TypeTooltip name="InboundMessage" type={`interface InboundMessage$1 {
readonly contact?: Contact$1;
readonly content: InboundContent;
readonly context?: MessageContext$1;
readonly errors: readonly WhatsAppApiError[];
readonly from: string;
readonly id: string;
readonly messageType: string;
readonly referral?: Referral$1;
readonly timestamp: Date;
}`} />. Narrow on `content.type` before reading content-specific fields:

```ts theme={null}
if (event.type === "message") {
  const { message } = event;

  switch (message.content.type) {
    case "text":
      console.log(message.content.body);
      break;
    case "image":
    case "video":
    case "audio":
    case "document":
      console.log(message.content.media.id);
      const { url } = await client.media.getUrl(message.content.media.id);
      break;
    case "location":
      console.log(message.content.location.latitude);
      break;
    case "reaction":
      console.log(message.content.reaction.emoji);
      break;
    case "interactive":
      // button / list / flow reply — see Interactive messages
      break;
    case "order":
      console.log(message.content.order.productItems);
      break;
  }
}
```

### Inbound content variants

<Accordion title="InboundContent" description="Every content shape a WhatsApp user can send.">
  | Variant         | Fields        |
  | --------------- | ------------- |
  | `"text"`        | `body`        |
  | `"image"`       | `media`       |
  | `"video"`       | `media`       |
  | `"audio"`       | `media`       |
  | `"document"`    | `media`       |
  | `"sticker"`     | `sticker`     |
  | `"location"`    | `location`    |
  | `"contacts"`    | `contacts`    |
  | `"reaction"`    | `reaction`    |
  | `"interactive"` | `interactive` |
  | `"button"`      | `button`      |
  | `"order"`       | `order`       |
  | `"system"`      | `system`      |
  | `"unknown"`     | —             |
</Accordion>

### Replying to an incoming message

Thread your reply with `replyTo: message.id`:

```ts theme={null}
if (event.type === "message" && event.message.content.type === "text") {
  await client.messages.send({
    to: event.message.from,
    replyTo: event.message.id,
    text: `You said: ${event.message.content.body}`,
  });
}
```

## Status updates

When `event.type === "status"`, the event carries a <TypeTooltip name="StatusUpdate" type={`interface StatusUpdate$1 {
readonly bizOpaqueCallbackData?: string;
readonly conversation?: Conversation$1;
readonly errors: readonly WhatsAppApiError[];
readonly id: string;
readonly pricing?: Pricing$1;
readonly recipientId: string;
readonly status: MessageStatus$1;
readonly timestamp: Date;
}`} />. The `status` field progresses through `sent → delivered → read` (or `played` for voice notes, or `failed`):

```ts theme={null}
if (event.type === "status") {
  const { status } = event;
  console.log(`${status.id}: ${status.status}`);
  if (status.status === "failed") {
    for (const err of status.errors) {
      console.error(err.code, err.title, err.message);
    }
  }
}
```

`status.bizOpaqueCallbackData` echoes whatever you passed on the original `send()` call — use it to correlate the status back to your own order/ticket/session ID.

## Reconnection

`subscribe()` reconnects automatically. Tune the backoff via `options.reconnect`:

<Accordion title="ReconnectOptions" description="">
  | Option         | Type                           | Description                                                               |
  | -------------- | ------------------------------ | ------------------------------------------------------------------------- |
  | `initialDelay` | `number`                       | Initial delay in milliseconds before the first reconnect. Default `1000`. |
  | `maxAttempts`  | `number`                       | Maximum number of consecutive reconnect attempts. Default `Infinity`.     |
  | `maxDelay`     | `number`                       | Maximum delay in milliseconds between retries. Default `30000`.           |
  | `multiplier`   | `number`                       | Multiplier applied to the delay after each failed attempt. Default `2`.   |
  | `onReconnect`  | `(attempt: number) =&gt; void` | Callback invoked before each reconnect attempt.                           |
</Accordion>

```ts theme={null}
client.events.subscribe({
  cursor: lastCursor,
  reconnect: {
    initialDelay: 500,
    maxDelay: 60_000,
    maxAttempts: 10,
    onReconnect: (attempt) => console.log(`reconnect attempt ${attempt}`),
  },
});
```

On reconnect, the stream internally calls `fetchMissedEvents` with the last cursor it saw, so you don't lose events that arrived while you were offline.

## Fetching missed events manually

If your client crashed without draining the stream, fetch missed events on startup before resubscribing:

```ts theme={null}
const { events } = await client.events.fetchMissed({
  cursor: lastCursor,
  limit: 100,
});

for (const event of events) {
  // replay in order
}

// then resume live subscription
for await (const event of client.events.subscribe({ cursor: lastCursor })) {
  // ...
}
```

Missed events are verified against your `app_secret` server-side before being returned — payloads that fail verification are dropped.

## Message context, referrals, errors

Every `InboundMessage` can also carry:

* `context` — reply-to metadata if the user replied to one of your earlier messages.
* `referral` — set when the user arrived via a Click-to-WhatsApp ad.
* `errors` — non-empty when Meta returned a partial error alongside the message.
* `contact` — the sender's display name (when available).
