Set up webhooks

After you send notifications, you can receive delivery and engagement events (delivered, open, click, failed, unsubscribe) at your own endpoint via the Events Webhook.

1. Add an endpoint

Create a publicly accessible HTTP endpoint that accepts POST requests and returns a 2xx status to acknowledge receipt.

2. Configure in the dashboard

  1. In the Pingram dashboard, go to Webhook.
  2. Enter your webhook URL (e.g. https://your-api.com/webhooks/pingram).
  3. Select the events you want (e.g. EMAIL_DELIVERED, EMAIL_OPEN, EMAIL_CLICK, EMAIL_FAILED).
  4. Save.

See Events Webhook for the full list of events and dashboard details.

3. What your endpoint receives

Pingram sends HTTP POST requests with a JSON body. Each request represents one event. Example for an email open:

{
  "eventType": "EMAIL_OPEN",
  "trackingId": "0192a1b2-c3d4-5e6f-7890-abcd1234ef56",
  "notificationId": "welcome-email",
  "channel": "EMAIL",
  "userId": "user-123"
}

Example for a delivery event:

{
  "eventType": "EMAIL_DELIVERED",
  "trackingId": "0192a1b2-c3d4-5e6f-7890-abcd1234ef56",
  "notificationId": "welcome-email",
  "channel": "EMAIL",
  "userId": "user-123"
}

For failure events, the body also includes a failureCode:

{
  "eventType": "EMAIL_FAILED",
  "trackingId": "0192a1b2-c3d4-5e6f-7890-abcd1234ef56",
  "notificationId": "welcome-email",
  "channel": "EMAIL",
  "userId": "user-123",
  "failureCode": "BOUNCE"
}

Return 2xx from your endpoint so Pingram knows the event was received.

Event lifecycle

After you send a notification, events are sent to your webhook URL as they occur (e.g. delivered, then open, then click). Each event is a separate POST with the JSON body above.

For the full list of events and payload fields (including clickedLink for clicks, and channel-specific failure codes), see Events Webhook.

4. Verify webhook signatures

Every webhook request includes three headers for signature verification:

  • X-Pingram-Id — Unique message ID for idempotency
  • X-Pingram-Signature — Versioned HMAC-SHA256 signature (format: v1,{hex})
  • X-Pingram-Timestamp — Unix timestamp in milliseconds

Your webhook secret (format: pingram_whsecret_...) is returned when you configure webhooks via the API (the secret field in the response).

Node.js (using the SDK):

import { NextRequest, NextResponse } from 'next/server';
import { verify, WebhookSignatureError } from 'pingram/webhooks';

export async function POST(req: NextRequest) {
  try {
    const event = verify({
      payload: await req.text(),
      headers: {
        id: req.headers.get('x-pingram-id')!,
        signature: req.headers.get('x-pingram-signature')!,
        timestamp: req.headers.get('x-pingram-timestamp')!
      },
      secret: process.env.PINGRAM_WEBHOOK_SECRET!
    });

    // event is typed as WebhookEvent
    console.log('Received event:', event.eventType);
    return NextResponse.json({ received: true });
  } catch (err) {
    if (err instanceof WebhookSignatureError) {
      return new NextResponse('Invalid signature', { status: 401 });
    }
    return new NextResponse('Bad request', { status: 400 });
  }
}

The WebhookEvent type is also available from the main 'pingram' package for type-only use.

For more details on signature verification, including manual verification for other languages, see Events Webhook — Verifying Webhooks.