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

# Polling Endpoints

> Pull storekit webhook events on your own schedule with a polling endpoint. Poll for a batch, process it, then commit the offset for the next poll.

A polling endpoint gives you the same stream of events as a webhook endpoint, but you fetch them with a GET request instead of storekit pushing them to a public URL. Each poll returns a batch of messages in the order they were received; you process the batch, commit how far you got, and the next poll continues from there.

## When to Use Polling

Polling suits cases where a public HTTPS endpoint is awkward or unnecessary:

* **Local development** — poll from your laptop without ngrok or a tunnel
* **Private networks** — the consuming service must not be reachable from the internet
* **Batch processing** — you want all of the day's events at once, for example to archive orders for compliance or to reconcile payouts overnight
* **Systems that cannot receive HTTP** — a cron job, a data pipeline or a script

If you need events within seconds of them happening, use a regular webhook endpoint. Polling and webhook endpoints can run side by side on the same account, filtered to the same or different event types.

## Creating a Polling Endpoint

1. Open **Settings** → **Webhooks**
2. In the embedded webhooks portal, add a new endpoint and choose the polling endpoint type
3. Select the event types you want to receive
4. Save the endpoint

The portal then shows two things you need for every request:

| Value | Example | Description |
| - | - | - |
| Polling URL | `https://api.svix.com/api/v1/app/app_xxx/polling-endpoint/poll_xxx/consumer/{consumer_id}` | Unique to this endpoint. Copy it exactly as shown — the host depends on the region — and replace `{consumer_id}` with your own identifier |
| API key | `sk_poll_*****` | Sent as a Bearer token. Shown once — store it securely |

<Warning>
  The API key grants read access to every event the endpoint is subscribed to, including customer names and contact details in order payloads. Treat it like any other production secret.
</Warning>

## Polling for Events

Send a GET request to the polling URL with your API key:

```bash theme={null}
curl -X GET "https://api.svix.com/api/v1/app/app_xxx/polling-endpoint/poll_xxx/consumer/kitchen-archive" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer sk_poll_*****"
```

The response is a batch of messages. Each message's `payload` is exactly the JSON body a webhook endpoint would receive — the `{ event, data }` envelope from the [payload format](/docs/developers/webhooks/payload-format), or the top-level shape of the [exceptions](/docs/developers/webhooks/payload-format) listed there — wrapped with message metadata and an `offset`:

```json theme={null}
{
  "data": [
    {
      "id": "msg_2K2N9Qk...",
      "eventType": "order.created",
      "payload": {
        "event": "order.created",
        "data": {
          "id": 123456,
          "status": "created",
          "venue": { "id": 789, "name": "Pizza Palace" },
          "total": 2450
        }
      },
      "timestamp": "2025-01-17T12:00:00.000Z",
      "offset": 0
    }
  ],
  "done": true
}
```

| Field | Description |
| - | - |
| `data` | Messages in the order they were received. Empty when there is nothing new |
| `data[].payload` | The webhook body: `payload.event` names the event, `payload.data` holds the event data (except for legacy `order.created` and `order.refund.*`, whose fields sit at the top level of `payload`) |
| `data[].offset` | Position of the message in the stream. Commit this value once processed |
| `done` | `true` when this batch reached the end of the stream; `false` means poll again straight away for more |

Signature verification does not apply — the request is authenticated by your API key rather than a signed delivery.

## Committing Your Position

After processing a batch, commit the `offset` of the last message you handled:

```bash theme={null}
curl -X POST "https://api.svix.com/api/v1/app/app_xxx/polling-endpoint/poll_xxx/consumer/kitchen-archive/commit" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk_poll_*****" \
  -d '{ "offset": 0 }'
```

Until you commit, the same messages are handed out again once their lease expires. That is deliberate: if your worker crashes mid-batch, nothing is lost. It also means your handler must be idempotent — see [Idempotency](/docs/developers/webhooks/advanced/idempotency).

## Consumer IDs

The `{consumer_id}` in the URL is a string you choose. Each consumer ID tracks its own position in the stream, so two consumers reading the same endpoint each receive every event.

* Use a **stable** ID per worker (`kitchen-archive`, `finance-reconciliation`) so progress survives restarts
* Never share one consumer ID between concurrently running clients. Two processes polling as the same consumer drift apart in the stream and the API returns errors

<Note>
  The stream starts when the polling endpoint is created. Events sent before that are not included.
</Note>

## A Typical Polling Loop

```javascript theme={null}
const URL = process.env.STOREKIT_POLL_URL;   // includes your consumer id
const KEY = process.env.STOREKIT_POLL_KEY;

async function pollOnce() {
  let done = false;
  while (!done) {
    const res = await fetch(URL, {
      headers: { Accept: 'application/json', Authorization: `Bearer ${KEY}` },
    });
    const body = await res.json();

    for (const msg of body.data) {
      await handleEvent(msg.eventType, msg.payload);
    }

    if (body.data.length > 0) {
      const last = body.data[body.data.length - 1];
      const commit = await fetch(`${URL}/commit`, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          Authorization: `Bearer ${KEY}`,
        },
        body: JSON.stringify({ offset: last.offset }),
      });
      if (!commit.ok) throw new Error(`Commit failed: ${commit.status}`);
    }

    done = body.done;
  }
}
```

Run `pollOnce()` on whatever schedule fits — every few seconds for near-real-time, or once a night for a daily export.

## Forwarding to a Private Service

If you would rather keep webhook-style HTTP delivery but your service is on a private network, run [Svix Bridge](https://github.com/svix/svix-webhooks/tree/main/bridge) inside that network. It polls the endpoint and forwards each message to an internal URL, RabbitMQ or Kafka, with retries when either side is temporarily unreachable. The polling endpoint's page in the embedded portal includes a starter Bridge configuration with the app ID and endpoint ID filled in.

## Related

<CardGroup cols={2}>
  <Card title="Setting Up Webhooks" icon="plug" href="/docs/developers/webhooks/setting-up-webhooks">
    Push delivery to a public HTTPS endpoint
  </Card>

  <Card title="Idempotency" icon="repeat" href="/docs/developers/webhooks/advanced/idempotency">
    Handle the same event more than once safely
  </Card>

  <Card title="Webhook Events" icon="list" href="/docs/developers/webhooks/webhook-events">
    Every event type you can subscribe to
  </Card>

  <Card title="Testing Webhooks" icon="flask" href="/docs/developers/webhooks/testing-webhooks">
    Inspect deliveries and send test events
  </Card>
</CardGroup>


## Related topics

- [MCP Endpoint](/docs/developers/agents/ordering/mcp-endpoint.md)
- [Orders Failing: Till Asleep](/docs/guides/integrations/pos/syrve/till-offline.md)
- [Custom Kitchen Ticket on a Star Cloud Printer](/docs/guides/build/recipes/custom-kitchen-ticket.md)
- [Data Warehouse Export](/docs/privacy-security/data-warehouse-export.md)
- [Audit Log](/docs/privacy-security/audit-log.md)


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