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

Notify webhook events
EventWhen
notification.acceptedA send was stored.
delivery.sentA provider accepted a delivery (push, web push, email).
delivery.deliveredInbox item written, or email accepted by the receiving server.
delivery.failedA delivery failed for good, with the reason.
delivery.bouncedAn email bounced or was marked as spam.
delivery.skippedA channel was not attempted, with the reason (opted out, no device…).
inbox_item.readThe recipient read an inbox item.
inbox_item.archivedThe recipient archived an inbox item.
inbox_item.interactedThe recipient pressed an item or one of its action buttons.
device.prunedA dead device token was removed.
device.movedA device token moved to another recipient (a shared device).
credential.invalidApple or Google rejected your APNs key or service account; that push channel is paused.
email.auto_pausedEmail paused for the workspace because of bounces or complaints.
recipient.deletedA 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_SECRET

3

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.