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

# Pull events

> Long-poll the same events as webhooks when you cannot expose a public HTTPS endpoint

# Pull events

Pull is the outbound-HTTPS alternative to webhooks. Use it when the consumer sits behind NAT, a firewall, or a private network and cannot receive WazzAPI callbacks.

Webhook is still the default when you have a reachable HTTPS URL. Pull delivers the same event types; it is not message-history pagination and not the provider webhook WazzAPI uses to ingest WhatsApp.

## When to use Pull vs webhooks

| Concern       | Webhook                  | Pull                                          |
| :------------ | :----------------------- | :-------------------------------------------- |
| Network       | You expose HTTPS         | You make outbound HTTPS requests              |
| Latency       | Lowest when healthy      | Poll interval; long polling is near-real-time |
| Failure model | WazzAPI retries delivery | You control cadence; seven-day backlog        |
| Recovery      | Retry state              | Re-request the last durable offset            |

## Create a subscription

Management calls use your organization API key. Creation is gated by `webhook_access`. Store the returned token immediately — plaintext is shown only on create and rotate.

```bash theme={null}
curl -X POST https://api.wazzapi.com/api/v1/pull/subscriptions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subscribed_events": ["message.received", "device.connection_failed"],
    "description": "private CRM consumer"
  }'
```

Official SDKs: `client.pull_subscriptions` (Python) and `client.pullSubscriptions` (Node).

## Poll

Polling uses **only** the Pull token (`Authorization: Bearer wzpull_...`). Do not poll with the org API key.

```bash theme={null}
curl "https://api.wazzapi.com/api/v1/pull/subscriptions/SUBSCRIPTION_ID/events?offset=210&limit=100&timeout=60" \
  -H "Authorization: Bearer wzpull_live_..." \
  -H "X-Wazzapi-Poller-ID: stable-client-process-id"
```

* `timeout=0` is a short poll. `1..60` enables long polling.
* SDKs set a transport timeout of server timeout + 10 seconds. Raw HTTP clients must do the same.
* A `200` means the batch was returned, not that your handler succeeded.
* Persist `next_offset` only after the entire batch is processed. Deduplicate by stable `event_id`.
* One active poller per subscription. A second poller gets `409 poller_conflict`.

## Offset rules

| Request                              | Result                                 |
| :----------------------------------- | :------------------------------------- |
| Current acknowledged offset          | Replay; do not advance                 |
| Most recently returned `next_offset` | Acknowledge previous batch, then fetch |
| Any other offset                     | `409 invalid_offset`                   |
| Offset older than retained data      | `410 cursor_expired`                   |

New subscriptions start at the current head. Existing events are not replayed.

## Skip a poison event

Only the current contiguous cursor head can be skipped. There is no automatic skip.

```bash theme={null}
curl -X POST https://api.wazzapi.com/api/v1/pull/subscriptions/SUBSCRIPTION_ID/events/SEQUENCE/skip \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Malformed payload; incident INC-123"}'
```

## SDK consumer

Both SDKs ship `PullSubscriptionConsumer` with a durable offset store. An in-memory integer is not enough across restarts.

<CodeGroup>
  ```ts Node.js theme={null}
  import { writeFile, readFile } from "node:fs/promises";
  import { WazzapiClient, PullSubscriptionConsumer } from "@wazzapi/wazzapi";

  const offsetStore = {
    async load() {
      try {
        return Number((await readFile(".pull-offset", "utf8")).trim() || "0");
      } catch {
        return null;
      }
    },
    async save(offset: number) {
      await writeFile(".pull-offset", String(offset));
    },
  };

  const client = new WazzapiClient({ apiKey: process.env.WAZZAPI_API_KEY });
  const created = await client.pullSubscriptions.create(
    ["message.received"],
    "private CRM consumer",
  );

  const consumer = new PullSubscriptionConsumer({
    client,
    subscriptionId: created.id,
    token: created.token ?? "",
    offsetStore,
    timeout: 50,
    onEvent: async (event) => {
      console.log(event.sequence, event.event_id, event.event_type);
    },
  });

  await consumer.run();
  ```

  ```python Python theme={null}
  from pathlib import Path
  from wazzapi import PullSubscriptionConsumer, WazzapiClient

  class FileOffsetStore:
      def __init__(self, path: Path) -> None:
          self._path = path

      def load(self) -> int | None:
          if not self._path.exists():
              return None
          return int(self._path.read_text().strip() or "0")

      def save(self, offset: int) -> None:
          self._path.write_text(str(offset))

  with WazzapiClient(api_key="your-api-key") as client:
      created = client.pull_subscriptions.create(
          ["message.received"],
          description="private CRM consumer",
      )
      consumer = PullSubscriptionConsumer(
          client=client,
          subscription_id=created.id,
          token=created.token,
          offset_store=FileOffsetStore(Path(".pull-offset")),
          on_event=lambda event: print(event.sequence, event.event_id, event.event_type),
          timeout=50,
      )
      consumer.run()
  ```
</CodeGroup>

See `examples/pull-events.ts` and `examples/pull_events.py` in the SDK repos.

## Error codes

`invalid_offset`, `poller_conflict`, `cursor_expired`, `subscription_paused`, `rate_limited`, `invalid_token`.

Limits: `limit` 1..100, `timeout` 0..60. Excessive short polling returns `429` with `Retry-After`.
