Webhooks
Updated Aug 2026 · API v3Register 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 signedping 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.