Pingram Webhooks
Receive real-time notifications about your messages via webhooks. Track delivery, opens, clicks, bounces, and more.
Setting Up Webhooks
- Go to Settings > Webhooks in the Pingram dashboard
- Click Add Webhook
- Enter your endpoint URL (must be HTTPS)
- Select the events you want to receive
- Save and test
Event Types
Email Events
| Event Type | Description |
|---|---|
EMAIL_DELIVERED |
Email delivered to recipient's server |
EMAIL_FAILED |
Email delivery failed |
EMAIL_OPEN |
Recipient opened the email |
EMAIL_CLICK |
Recipient clicked a link |
EMAIL_UNSUBSCRIBE |
Recipient unsubscribed |
EMAIL_INBOUND |
Received an inbound email |
SMS Events
| Event Type | Description |
|---|---|
SMS_DELIVERED |
SMS delivered to recipient |
SMS_FAILED |
SMS delivery failed |
SMS_UNSUBSCRIBE |
Recipient unsubscribed |
SMS_INBOUND |
Received an inbound SMS (paid accounts, dedicated number) |
Voice Events
| Event Type | Description |
|---|---|
CALL_FAILED |
Voice call failed |
CALL_UNSUBSCRIBE |
Recipient unsubscribed |
Push Events
| Event Type | Description |
|---|---|
PUSH_FAILED |
Mobile push delivery failed |
PUSH_UNSUBSCRIBE |
Recipient unsubscribed |
WEB_PUSH_FAILED |
Web push delivery failed |
WEB_PUSH_UNSUBSCRIBE |
Recipient unsubscribed |
Other Events
| Event Type | Description |
|---|---|
INAPP_WEB_FAILED |
In-app message failed |
INAPP_WEB_UNSUBSCRIBE |
Recipient unsubscribed |
SLACK_FAILED |
Slack message failed |
SLACK_UNSUBSCRIBE |
Recipient unsubscribed |
Note: Inbound events (
EMAIL_INBOUND,SMS_INBOUND) have a different payload structure. See the pingram-inbound skill for details.
Webhook Payload Structure
All webhook payloads use a flat structure with the following fields:
{
"eventType": "EMAIL_DELIVERED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "welcome_email",
"channel": "EMAIL",
"userId": "user@example.com"
}
| Field | Type | Description |
|---|---|---|
eventType |
string | The event type (see Event Types above) |
trackingId |
string | Unique ID for the notification (optional) |
notificationId |
string | The notification type |
channel |
string | EMAIL, SMS, CALL, PUSH, WEB_PUSH, INAPP_WEB, SLACK |
userId |
string | The recipient's user ID |
failureCode |
string | Error code (only for *_FAILED events) |
clickedLink |
string | URL clicked (only for EMAIL_CLICK) |
clickedLinkTags |
object | Link tags (only for EMAIL_CLICK) |
Note: Inbound events (
EMAIL_INBOUND,SMS_INBOUND) have a different payload structure with fields likefrom,to,text/bodyText,receivedAt, etc. See the pingram-inbound skill for detailed payload examples.
Event Payloads
EMAIL_DELIVERED
{
"eventType": "EMAIL_DELIVERED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "welcome",
"channel": "EMAIL",
"userId": "user@example.com"
}
EMAIL_FAILED
{
"eventType": "EMAIL_FAILED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "welcome",
"channel": "EMAIL",
"userId": "user@example.com",
"failureCode": "EMAIL_BOUNCE"
}
EMAIL_OPEN
{
"eventType": "EMAIL_OPEN",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "welcome",
"channel": "EMAIL",
"userId": "user@example.com"
}
EMAIL_CLICK
{
"eventType": "EMAIL_CLICK",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "welcome",
"channel": "EMAIL",
"userId": "user@example.com",
"clickedLink": "https://app.example.com/activate",
"clickedLinkTags": {
"campaign": ["onboarding"],
"action": ["activate"]
}
}
SMS_DELIVERED
{
"eventType": "SMS_DELIVERED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "otp",
"channel": "SMS",
"userId": "user@example.com"
}
SMS_FAILED
{
"eventType": "SMS_FAILED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "otp",
"channel": "SMS",
"userId": "user@example.com",
"failureCode": "INVALID_NUMBER"
}
CALL_FAILED
{
"eventType": "CALL_FAILED",
"trackingId": "019abc12-3456-7890-abcd-ef1234567890",
"notificationId": "alert",
"channel": "CALL",
"userId": "user@example.com",
"failureCode": "NO_ANSWER"
}
*_UNSUBSCRIBE
All unsubscribe events follow this format:
{
"eventType": "EMAIL_UNSUBSCRIBE",
"notificationId": "marketing",
"channel": "EMAIL",
"userId": "user@example.com"
}
Webhook Handler Example
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/pingram', (req, res) => {
const {
eventType,
trackingId,
notificationId,
channel,
userId,
failureCode,
clickedLink
} = req.body;
switch (eventType) {
case 'EMAIL_DELIVERED':
console.log(`Email delivered to ${userId}`);
// Update delivery status in your database
break;
case 'EMAIL_FAILED':
console.log(`Email failed: ${failureCode}`);
// Handle bounce, mark email as invalid if hard bounce
break;
case 'EMAIL_OPEN':
console.log(`Email opened by ${userId}`);
// Track engagement metrics
break;
case 'EMAIL_CLICK':
console.log(`Link clicked: ${clickedLink}`);
// Track click-through rates
break;
case 'SMS_DELIVERED':
console.log(`SMS delivered to ${userId}`);
break;
case 'SMS_FAILED':
console.log(`SMS failed: ${failureCode}`);
break;
case 'EMAIL_UNSUBSCRIBE':
case 'SMS_UNSUBSCRIBE':
console.log(`${userId} unsubscribed from ${channel}`);
// Update user preferences
break;
default:
console.log(`Unhandled event: ${eventType}`);
}
// Always respond quickly
res.status(200).send('OK');
});
app.listen(3000);
Best Practices
1. Respond Quickly
app.post('/webhooks/pingram', async (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
2. Handle Duplicates
Use trackingId for idempotency:
app.post('/webhooks/pingram', async (req, res) => {
const { eventType, trackingId } = req.body;
const eventKey = `${eventType}:${trackingId}`;
// Check if already processed
if (await redis.exists(eventKey)) {
return res.status(200).send('Already processed');
}
// Mark as processing
await redis.setex(eventKey, 86400, '1');
// Process event...
res.status(200).send('OK');
});
3. Use Queues for Heavy Processing
import { Queue } from 'bullmq';
const webhookQueue = new Queue('webhooks');
app.post('/webhooks/pingram', async (req, res) => {
// Add to queue for processing
await webhookQueue.add('process', req.body);
res.status(200).send('OK');
});
Testing Webhooks
Using the Dashboard
- Go to Settings > Webhooks
- Click "Test" next to your webhook
- Select an event type
- Send test payload
Local Development
Use a tunneling service like ngrok:
ngrok http 3000
Then use the ngrok URL as your webhook endpoint.
Common Issues
Not receiving webhooks:
- Verify URL is correct and HTTPS
- Check firewall allows incoming connections
- Ensure your server returns 2xx status
Duplicate events:
- Implement idempotency using trackingId
- Store processed event IDs temporarily
Webhook delays:
- Check your server response time
- Ensure you're responding with 200 quickly