developers  · docs
Get your token
Docs · Webhooks

Webhooks

Updated Aug 2026 · API v3

Register an HTTPS endpoint and pon calls you when something happens — no polling loop, no wasted requests. Manage webhooks at my.pon.app → Webhooks or via the API.

Topics

Topic Fires when
list.changed Anything about a list changed (coarse, coalesced — the default)
item.added An item landed on a list
item.removed An item was deleted
item.checked An item was checked off while shopping (un-checking only fires list.changed)
list.members.changed Someone joined or left a list

Subscribe to what you actually need. An optional listIds filter (up to 50, your own lists only — unknown ids answer 400 with invalidListIds) narrows deliveries to specific lists.

Register

POST /v3/webhooks

curl -X POST https://api.pon.app/v3/webhooks \
  -H "Authorization: Bearer pon_pat_XXXX" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.example.com/pon",
        "events": ["item.checked"],
        "listIds": ["LIST_ID"] }'

The response contains your signing secret (whsec_…) — shown once. Re-registering the same URL updates the subscription and keeps the secret. Up to 5 webhooks per account.

Registration pings your URL immediately.

pon sends a signed ping event and expects a 2xx — answer it before checking the signature (the secret only arrives in the response). A dead URL is rejected with 422 WEBHOOK_UNREACHABLE.

Deliveries

Each delivery is a POST with headers PON-Event: <topic> and PON-Signature: sha256=<hmac>:

{ "event": "item.checked", "listId": "6led…", "cursor": 123, "at": "2026-08-09T19:00:00.000Z" }

Deliveries are best-effort with no retries — if your endpoint is down, the next event (or a fetch on your side) catches you up. Redirects are not followed. Several matching changes in one write coalesce into one delivery per subscribed topic.

Verify the signature

Compute an HMAC-SHA256 over the raw request body with your secret and compare it — constant-time — against the header:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const expected = "sha256=" +
    createHmac("sha256", secret).update(rawBody).digest("hex");
  return (
    header.length === expected.length &&
    timingSafeEqual(Buffer.from(header), Buffer.from(expected))
  );
}
import hashlib, hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(header, expected)

Circuit breaker

A 2xx acknowledges a delivery. After 20 consecutive failures (anything else, including timeouts) the webhook auto-pauses (status: paused, pausedReason: auto_failures). Fix your endpoint, then reactivate at my.pon.app or via PATCH /v3/webhooks/{id} with { "status": "active" } — reactivating resets the counter. A manual test ping is POST /v3/webhooks/{id}/pings (never counts as a failure).

Webhooks beat polling — always.

A 5-minute polling loop makes ~8,600 requests a month and reads as heavy use on the fair-use gauge. A webhook makes exactly as many as your lists change.