Notify guides
15 · Webhooks
Hear what happened.
Signed events for deliveries, inbox reads, pruned devices, invalid credentials and erasures, so your app and your on-call know without polling.
You need notify.webhook.manage. Signatures, retries, auto-disable and replay work the same for every ProductCraft product: see the shared Webhooks page. Payloads carry ids, keys, channels, states and reasons, never rendered message content.
1
The events
| Event | When |
|---|---|
notification.accepted | A send was stored. |
delivery.sent | A provider accepted a delivery (push, web push, email). |
delivery.delivered | Inbox item written, or email accepted by the receiving server. |
delivery.failed | A delivery failed for good, with the reason. |
delivery.bounced | An email bounced or was marked as spam. |
delivery.skipped | A channel was not attempted, with the reason (opted out, no device…). |
inbox_item.read | The recipient read an inbox item. |
inbox_item.archived | The recipient archived an inbox item. |
inbox_item.interacted | The recipient pressed an item or one of its action buttons. |
device.pruned | A dead device token was removed. |
device.moved | A device token moved to another recipient (a shared device). |
credential.invalid | Apple or Google rejected your APNs key or service account; that push channel is paused. |
email.auto_paused | Email paused for the workspace because of bounces or complaints. |
recipient.deleted | A recipient was erased. |
2
Add an endpoint
create-endpoint.ts
const endpoint = await notify.webhooks.endpoints.create({
url: "https://app.example.com/webhooks/notify",
event_types: ["delivery.failed", "device.pruned", "credential.invalid", "email.auto_paused"],
});
console.log(endpoint.signing_secret); // shown once: store it as PRODUCTCRAFT_NOTIFY_WEBHOOK_SECRET3
Verify and react
notify.webhooks.unwrap() checks the Standard Webhooks signature against the raw body and returns a typed event. In tests, notify.webhooks.sign() signs a fake event the same way.
app/webhooks/notify/route.ts
// app/webhooks/notify/route.ts
import { Notify } from "@productcraft/notify";
const notify = new Notify(); // reads PRODUCTCRAFT_NOTIFY_WEBHOOK_SECRET
export async function POST(req: Request) {
const event = await notify.webhooks.unwrap({ payload: await req.text(), headers: req.headers });
switch (event.type) {
case "delivery.failed":
console.log(event.data.recipient, event.data.channel, event.data.reason);
break;
case "credential.invalid":
// An APNs key was revoked or a service account deleted: push is paused.
console.error("re-upload", event.data.provider, event.data.app_id, event.data.error_code);
break;
case "recipient.deleted":
console.log("erased", event.data.external_id);
break;
default:
break; // newer event types arrive as UnknownWebhookEvent
}
return new Response(null, { status: 204 });
}4
Order and duplicates
- Events can arrive out of order and more than once: skip a repeated
webhook-id. - The message log is the record; a missed webhook loses nothing, and events can be replayed.