Guides

Polling without webhooks

A cron job and one call: GET /v1/changes tells you every invoice, deposit and payout that changed since you last asked. Every plan has it.

Webhooks push events to your server the moment they happen and are included from the Growth plan up. On Starter, or anywhere you would rather not run a public endpoint, poll instead. You lose nothing but latency: the same event log that feeds webhooks feeds this.

One call per tick

GET /v1/changes?after=918273
Authorization: Bearer mplive_…
{
  "object": "changes",
  "livemode": true,
  "invoices": [
    {
      "id": "inv_01K3…",
      "last_event": "invoice.paid",
      "sequence": 918301,
      "updated": "2026-09-23T10:41:12.004Z",
      "data": { /* the invoice, exactly as GET /v1/invoices/inv_01K3… returns it */ }
    }
  ],
  "deposits": [ /* same shape */ ],
  "payouts":  [ /* same shape */ ],
  "customers": [],
  "wallets": [],
  "other": [ /* raw events with no resource shape: balance transfers, conversions… */ ],
  "events_scanned": 12,
  "has_more": false,
  "next_after": 918301
}

Store next_after. Send it back as after next time. That is the whole protocol.

The rules that make it safe

  • The cursor is the event sequence — a per-project counter that only goes up. Two ticks can never see the same change twice or skip one, however long you were down. Gaps in the numbers are normal.
  • Each resource appears once, in its latest state. An invoice created, quoted and paid between two ticks comes back as one paid invoice; last_event says which event that was. You never have to replay a sequence of events to work out where something ended up.
  • data is the REST shape. The same deserializer you wrote for GET /v1/invoices/{id} reads it.
  • has_more: true means the page filled before the log ran out. Call again immediately with the new next_after; do not wait for the next cron tick. A call scans 200 events by default and at most 500 (limit).
  • Filter by type if you only care about money in: ?after=…&type=invoice.paid&type=deposit.confirmed.

The first call

Send nothing to read the whole log from the beginning, or ?since=2026-09-23T00:00:00Z to start from an instant. Either way the response's next_after is what you store; from then on you only ever send after.

Which key

Any key with the webhooks:read scope — it is the log-reading scope and is granted by default. Live keys read live changes, test keys read test changes.

When you move to webhooks

Nothing to re-learn: a webhook envelope carries the same sequence, so the cursor you stored is where you reconcile from. Keep the poller running for a day as a check that every event arrived, then switch it off.