Instantly Webhooks & Events
Overview
Handle Instantly API v2 webhooks for real-time email outreach event notifications. Instantly fires events when emails are sent, opened, clicked, replied to, or bounced, and when leads change interest status. Webhooks require Hypergrowth plan ($97/mo) or higher. Delivery retries: 3 times within 30 seconds on failure.
Prerequisites
- Instantly Hypergrowth plan or higher (required for webhooks)
- API key with
all:all or appropriate webhook scopes
- Public HTTPS endpoint for receiving webhook payloads
INSTANTLY_API_KEY environment variable set
Webhook Event Types
| Event Type |
Trigger |
Key Payload Fields |
email_sent |
Email delivered to recipient |
lead_email, campaign_id, step |
email_opened |
Recipient opens email |
lead_email, campaign_id, open_count |
email_link_clicked |
Recipient clicks a link |
lead_email, campaign_id, link_url |
reply_received |
Recipient replies |
lead_email, campaign_id, reply_text |
email_bounced |
Email bounces |
lead_email, bounce_type, reason |
lead_unsubscribed |
Lead unsubscribes |
lead_email, campaign_id |
campaign_completed |
All leads in campaign processed |
campaign_id, campaign_name |
account_error |
Sending account error |
email, error_type |
lead_interested |
Lead marked interested |
lead_email, campaign_id |
lead_not_interested |
Lead marked not interested |
lead_email, campaign_id |
lead_meeting_booked |
Meeting booked |
lead_email, campaign_id |
lead_meeting_completed |
Meeting completed |
lead_email |
lead_closed |
Lead closed/won |
lead_email |
lead_out_of_office |
OOO reply detected |
lead_email |
lead_wrong_person |
Wrong person response |
lead_email |
all_events |
Subscribe to everything |
Varies by event |
Instructions
Step 1: Create Webhook via API
import { instantly } from "./src/instantly";
async function createWebhook() {
// Create webhook for specific events
const webhook = await instantly<{ id: string; name: string }>("/webhooks", {
method: "POST",
body: JSON.stringify({
name: "CRM Sync — Replies & Meetings",
target_hook_url: "https://api.yourapp.com/webhooks/instantly",
event_type: "reply_received",
headers: {
"X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET,
},
}),
});
console.log(`Webhook created: ${webhook.id}`);
// Create additional webhooks for other events
for (const event of ["lead_interested", "lead_meeting_booked", "email_bounced"]) {
await instantly("/webhooks", {
method: "POST",
body: JSON.stringify({
name: `CRM Sync — ${event}`,
target_hook_url: "https://api.yourapp.com/webhooks/instantly",
event_type: event,
headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },
}),
});
}
// Or subscribe to ALL events with one webhook
await instantly("/webhooks", {
method: "POST",
body: JSON.stringify({
name: "All Events Monitor",
target_hook_url: "https://api.yourapp.com/webhooks/instantly/all",
event_type: "all_events",
headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },
}),
});
}
Step 2: Build Event Handler
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/instantly", async (req, res) => {
// Validate secret
if (req.headers["x-webhook-secret"] !== process.env.INSTANTLY_WEBHOOK_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
// Respond 200 immediately — Instantly retries 3x in 30s on failure
res.status(200).json({ received: true });
const { event_type, data } = req.body;
console.log(`Event: ${event_type}`, JSON.stringify(data).slice(0, 300));
try {
await routeEvent(event_type, data);
} catch (err) {
console.error(`Failed to process ${event_type}:`, err);
}
});
async function routeEvent(eventType: string, data: any) {
switch (eventType) {
case "reply_received":
await handleReply(data);
break;
case "email_bounced":
await handleBounce(data);
break;
case "lead_interested":
case "lead_meeting_booked":
case "lead_closed":
await handlePositiveOutcome(eventType, data);
break;
case "lead_unsubscribed":
await handleUnsubscribe(data);
break;
case "campaign_completed":
await handleCampaignComplete(data);
break;
case "account_error":
await handleAccountError(data);
break;
default:
console.log(`Unhandled event: ${eventType}`);
}
}
Step 3: Implement Event Handlers
async function handleReply(data: {
lead_email: string;
campaign_id: string;
reply_text: string;
}) {
console.log(`Reply from ${data.lead_email} in campaign ${data.campaign_id}`);
// Sync to CRM
await crmClient.updateContact(data.lead_email, {
status: "replied",
lastReply: data.reply_text,
lastActivity: new Date(),
});
// Notify sales team
await slackNotify("#sales-replies", {
text: `Reply from ${data.lead_email}:\n${data.reply_text.slice(0, 500)}`,
});
}
async function handleBounce(data: {
lead_email: string;
bounce_type: string;
reason: string;
}) {
console.log(`Bounce: ${data.lead_email} (${data.bounce_type})`);
if (data.bounce_type === "hard") {
// Add to global block list
await instantly("/block-lists-entries", {
method: "POST",
body: JSON.stringify({ bl_value: data.lead_email }),
});
console.log(`Added ${data.lead_email} to block list`);
}
}
async function handlePositiveOutcome(
eventType: string,
data: { lead_email: string; campaign_id: string }
) {
const statusMap: Record<string, string> = {
lead_interested: "interested",
lead_meeting_booked: "meeting_scheduled",
lead_closed: "closed_won",
};
await crmClient.updateContact(data.lead_email, {
status: statusMap[eventType] || eventType,
lastActivity: new Date(),
});
if (eventType === "lead_meeting_booked") {
await slackNotify("#sales-wins", {
text: `Meeting booked with ${data.lead_email}!`,
});
}
}
async function handleUnsubscribe(data: { lead_email: string }) {
// Add to block list to prevent future outreach across all campaigns
await instantly("/block-lists-entries", {
method: "POST",
body: JSON.stringify({ bl_value: data.lead_email }),
});
console.log(`Unsubscribed + blocked: ${data.lead_email}`);
}
async function handleCampaignComplete(data: { campaign_id: string }) {
// Pull final analytics
const analytics = await instantly(`/campaigns/analytics?id=${data.campaign_id}`);
console.log(`Campaign complete:`, analytics);
}
async function handleAccountError(data: { email: string; error_type: string }) {
console.error(`Account error: ${data.email} — ${data.error_type}`);
await slackNotify("#ops-alerts", {
text: `Instantly account error: ${data.email}\nType: ${data.error_type}`,
});
}
Step 4: Manage Webhooks
// List all webhooks
async function listWebhooks() {
const webhooks = await instantly<Array<{
id: string; name: string; event_type: string; target_hook_url: string;
}>>("/webhooks?limit=50");
for (const w of webhooks) {
console.log(`${w.id}: ${w.name} [${w.event_type}] -> ${w.target_hook_url}`);
}
}
// Test a webhook
async function testWebhook(webhookId: string) {
await instantly(`/webhooks/${webhookId}/test`, { method: "POST" });
}
// Resume a paused webhook
async function resumeWebhook(webhookId: string) {
await instantly(`/webhooks/${webhookId}/resume`, { method: "POST" });
}
// Check delivery status
async function checkDeliveryHealth() {
const summary = await instantly("/webhook-events/summary");
console.log("Webhook delivery summary:", summary);
const byDate = await instantly("/webhook-events/summary-by-date");
console.log("By date:", byDate);
}
// Delete a webhook
async function deleteWebhook(webhookId: string) {
await instantly(`/webhooks/${webhookId}`, { method: "DELETE" });
}
Key API Endpoints
| Method |
Path |
Purpose |
POST |
/webhooks |
Create webhook subscription |
GET |
/webhooks |
List webhooks |
PATCH |
/webhooks/{id} |
Update webhook |
DELETE |
/webhooks/{id} |
Delete webhook |
POST |
/webhooks/{id}/test |
Send test event |
POST |
/webhooks/{id}/resume |
Resume paused webhook |
GET |
/webhook-events |
List webhook events |
GET |
/webhook-events/summary |
Delivery summary |
Error Handling
| Issue |
Cause |
Solution |
| No events delivered |
Webhook not registered or paused |
Check GET /webhooks, resume if paused |
| Duplicate events |
Retry delivery |
Deduplicate by event ID + timestamp |
| Webhook paused automatically |
Too many delivery failures |
Fix endpoint, then POST /webhooks/{id}/resume |
| 30s timeout |
Handler takes too long |
Return 200 immediately, process async |
| Missing event_type |
Using custom label events |
Check custom_interest_value field |
Resources
Next Steps
For performance optimization, see instantly-performance-tuning.
Source: jeremylongshore/claude-code-plugins-plus-skills → plugins/saas-packs/instantly-pack/skills/instantly-webhooks-events/SKILL.md
1---2name: instantly-webhooks-events3description: 'Implement Instantly.ai webhook event handling with real API v2 event types. Use when setting up webhook endpoints, processing email events, or building CRM sync pipelines from Instantly notifications. Trigger with phrases like "instantly webhook", "instantly events", "instantly webhook handler", "handle instantly events", "instantly notifications". '4---56# Instantly Webhooks & Events78## Overview910Handle Instantly API v2 webhooks for real-time email outreach event notifications. Instantly fires events when emails are sent, opened, clicked, replied to, or bounced, and when leads change interest status. Webhooks require Hypergrowth plan ($97/mo) or higher. Delivery retries: **3 times within 30 seconds** on failure.1112## Prerequisites1314- Instantly Hypergrowth plan or higher (required for webhooks)15- API key with `all:all` or appropriate webhook scopes16- Public HTTPS endpoint for receiving webhook payloads17- `INSTANTLY_API_KEY` environment variable set1819## Webhook Event Types2021| Event Type | Trigger | Key Payload Fields |22|------------|---------|-------------------|23| `email_sent` | Email delivered to recipient | `lead_email`, `campaign_id`, `step` |24| `email_opened` | Recipient opens email | `lead_email`, `campaign_id`, `open_count` |25| `email_link_clicked` | Recipient clicks a link | `lead_email`, `campaign_id`, `link_url` |26| `reply_received` | Recipient replies | `lead_email`, `campaign_id`, `reply_text` |27| `email_bounced` | Email bounces | `lead_email`, `bounce_type`, `reason` |28| `lead_unsubscribed` | Lead unsubscribes | `lead_email`, `campaign_id` |29| `campaign_completed` | All leads in campaign processed | `campaign_id`, `campaign_name` |30| `account_error` | Sending account error | `email`, `error_type` |31| `lead_interested` | Lead marked interested | `lead_email`, `campaign_id` |32| `lead_not_interested` | Lead marked not interested | `lead_email`, `campaign_id` |33| `lead_meeting_booked` | Meeting booked | `lead_email`, `campaign_id` |34| `lead_meeting_completed` | Meeting completed | `lead_email` |35| `lead_closed` | Lead closed/won | `lead_email` |36| `lead_out_of_office` | OOO reply detected | `lead_email` |37| `lead_wrong_person` | Wrong person response | `lead_email` |38| `all_events` | Subscribe to everything | Varies by event |3940## Instructions4142### Step 1: Create Webhook via API4344```typescript45import { instantly } from "./src/instantly";4647async function createWebhook() {48 // Create webhook for specific events49 const webhook = await instantly<{ id: string; name: string }>("/webhooks", {50 method: "POST",51 body: JSON.stringify({52 name: "CRM Sync — Replies & Meetings",53 target_hook_url: "https://api.yourapp.com/webhooks/instantly",54 event_type: "reply_received",55 headers: {56 "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET,57 },58 }),59 });60 console.log(`Webhook created: ${webhook.id}`);6162 // Create additional webhooks for other events63 for (const event of ["lead_interested", "lead_meeting_booked", "email_bounced"]) {64 await instantly("/webhooks", {65 method: "POST",66 body: JSON.stringify({67 name: `CRM Sync — ${event}`,68 target_hook_url: "https://api.yourapp.com/webhooks/instantly",69 event_type: event,70 headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },71 }),72 });73 }7475 // Or subscribe to ALL events with one webhook76 await instantly("/webhooks", {77 method: "POST",78 body: JSON.stringify({79 name: "All Events Monitor",80 target_hook_url: "https://api.yourapp.com/webhooks/instantly/all",81 event_type: "all_events",82 headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },83 }),84 });85}86```8788### Step 2: Build Event Handler8990```typescript91import express from "express";9293const app = express();94app.use(express.json());9596app.post("/webhooks/instantly", async (req, res) => {97 // Validate secret98 if (req.headers["x-webhook-secret"] !== process.env.INSTANTLY_WEBHOOK_SECRET) {99 return res.status(401).json({ error: "Unauthorized" });100 }101102 // Respond 200 immediately — Instantly retries 3x in 30s on failure103 res.status(200).json({ received: true });104105 const { event_type, data } = req.body;106 console.log(`Event: ${event_type}`, JSON.stringify(data).slice(0, 300));107108 try {109 await routeEvent(event_type, data);110 } catch (err) {111 console.error(`Failed to process ${event_type}:`, err);112 }113});114115async function routeEvent(eventType: string, data: any) {116 switch (eventType) {117 case "reply_received":118 await handleReply(data);119 break;120 case "email_bounced":121 await handleBounce(data);122 break;123 case "lead_interested":124 case "lead_meeting_booked":125 case "lead_closed":126 await handlePositiveOutcome(eventType, data);127 break;128 case "lead_unsubscribed":129 await handleUnsubscribe(data);130 break;131 case "campaign_completed":132 await handleCampaignComplete(data);133 break;134 case "account_error":135 await handleAccountError(data);136 break;137 default:138 console.log(`Unhandled event: ${eventType}`);139 }140}141```142143### Step 3: Implement Event Handlers144145```typescript146async function handleReply(data: {147 lead_email: string;148 campaign_id: string;149 reply_text: string;150}) {151 console.log(`Reply from ${data.lead_email} in campaign ${data.campaign_id}`);152153 // Sync to CRM154 await crmClient.updateContact(data.lead_email, {155 status: "replied",156 lastReply: data.reply_text,157 lastActivity: new Date(),158 });159160 // Notify sales team161 await slackNotify("#sales-replies", {162 text: `Reply from ${data.lead_email}:\n${data.reply_text.slice(0, 500)}`,163 });164}165166async function handleBounce(data: {167 lead_email: string;168 bounce_type: string;169 reason: string;170}) {171 console.log(`Bounce: ${data.lead_email} (${data.bounce_type})`);172173 if (data.bounce_type === "hard") {174 // Add to global block list175 await instantly("/block-lists-entries", {176 method: "POST",177 body: JSON.stringify({ bl_value: data.lead_email }),178 });179 console.log(`Added ${data.lead_email} to block list`);180 }181}182183async function handlePositiveOutcome(184 eventType: string,185 data: { lead_email: string; campaign_id: string }186) {187 const statusMap: Record<string, string> = {188 lead_interested: "interested",189 lead_meeting_booked: "meeting_scheduled",190 lead_closed: "closed_won",191 };192193 await crmClient.updateContact(data.lead_email, {194 status: statusMap[eventType] || eventType,195 lastActivity: new Date(),196 });197198 if (eventType === "lead_meeting_booked") {199 await slackNotify("#sales-wins", {200 text: `Meeting booked with ${data.lead_email}!`,201 });202 }203}204205async function handleUnsubscribe(data: { lead_email: string }) {206 // Add to block list to prevent future outreach across all campaigns207 await instantly("/block-lists-entries", {208 method: "POST",209 body: JSON.stringify({ bl_value: data.lead_email }),210 });211 console.log(`Unsubscribed + blocked: ${data.lead_email}`);212}213214async function handleCampaignComplete(data: { campaign_id: string }) {215 // Pull final analytics216 const analytics = await instantly(`/campaigns/analytics?id=${data.campaign_id}`);217 console.log(`Campaign complete:`, analytics);218}219220async function handleAccountError(data: { email: string; error_type: string }) {221 console.error(`Account error: ${data.email} — ${data.error_type}`);222 await slackNotify("#ops-alerts", {223 text: `Instantly account error: ${data.email}\nType: ${data.error_type}`,224 });225}226```227228### Step 4: Manage Webhooks229230```typescript231// List all webhooks232async function listWebhooks() {233 const webhooks = await instantly<Array<{234 id: string; name: string; event_type: string; target_hook_url: string;235 }>>("/webhooks?limit=50");236237 for (const w of webhooks) {238 console.log(`${w.id}: ${w.name} [${w.event_type}] -> ${w.target_hook_url}`);239 }240}241242// Test a webhook243async function testWebhook(webhookId: string) {244 await instantly(`/webhooks/${webhookId}/test`, { method: "POST" });245}246247// Resume a paused webhook248async function resumeWebhook(webhookId: string) {249 await instantly(`/webhooks/${webhookId}/resume`, { method: "POST" });250}251252// Check delivery status253async function checkDeliveryHealth() {254 const summary = await instantly("/webhook-events/summary");255 console.log("Webhook delivery summary:", summary);256257 const byDate = await instantly("/webhook-events/summary-by-date");258 console.log("By date:", byDate);259}260261// Delete a webhook262async function deleteWebhook(webhookId: string) {263 await instantly(`/webhooks/${webhookId}`, { method: "DELETE" });264}265```266267## Key API Endpoints268269| Method | Path | Purpose |270|--------|------|---------|271| `POST` | `/webhooks` | Create webhook subscription |272| `GET` | `/webhooks` | List webhooks |273| `PATCH` | `/webhooks/{id}` | Update webhook |274| `DELETE` | `/webhooks/{id}` | Delete webhook |275| `POST` | `/webhooks/{id}/test` | Send test event |276| `POST` | `/webhooks/{id}/resume` | Resume paused webhook |277| `GET` | `/webhook-events` | List webhook events |278| `GET` | `/webhook-events/summary` | Delivery summary |279280## Error Handling281282| Issue | Cause | Solution |283|-------|-------|----------|284| No events delivered | Webhook not registered or paused | Check `GET /webhooks`, resume if paused |285| Duplicate events | Retry delivery | Deduplicate by event ID + timestamp |286| Webhook paused automatically | Too many delivery failures | Fix endpoint, then `POST /webhooks/{id}/resume` |287| 30s timeout | Handler takes too long | Return 200 immediately, process async |288| Missing event_type | Using custom label events | Check `custom_interest_value` field |289290## Resources291292- Instantly Webhook API293- Instantly Webhook Events294- [Instantly Blog: Webhooks Guide](https://instantly.ai/blog/api-webhooks-custom-integrations-for-outreach/)295296## Next Steps297298For performance optimization, see `instantly-performance-tuning`.299300---301302**Source:** [`jeremylongshore/claude-code-plugins-plus-skills`](https://github.com/jeremylongshore/claude-code-plugins-plus-skills) → `plugins/saas-packs/instantly-pack/skills/instantly-webhooks-events/SKILL.md`