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\". '"4allowed-tools: "Read, Write, Edit, Bash(curl:*), Bash(npm:*), Grep"5category: backend-and-data6source_repo: jeremylongshore/claude-code-plugins-plus-skills7source_path: "plugins/saas-packs/instantly-pack/skills/instantly-webhooks-events/SKILL.md"8source_url: https://github.com/jeremylongshore/claude-code-plugins-plus-skills/blob/HEAD/plugins/saas-packs/instantly-pack/skills/instantly-webhooks-events/SKILL.md9---1011# Instantly Webhooks & Events1213## Overview1415Handle 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.1617## Prerequisites1819- Instantly Hypergrowth plan or higher (required for webhooks)20- API key with `all:all` or appropriate webhook scopes21- Public HTTPS endpoint for receiving webhook payloads22- `INSTANTLY_API_KEY` environment variable set2324## Webhook Event Types2526| Event Type | Trigger | Key Payload Fields |27|------------|---------|-------------------|28| `email_sent` | Email delivered to recipient | `lead_email`, `campaign_id`, `step` |29| `email_opened` | Recipient opens email | `lead_email`, `campaign_id`, `open_count` |30| `email_link_clicked` | Recipient clicks a link | `lead_email`, `campaign_id`, `link_url` |31| `reply_received` | Recipient replies | `lead_email`, `campaign_id`, `reply_text` |32| `email_bounced` | Email bounces | `lead_email`, `bounce_type`, `reason` |33| `lead_unsubscribed` | Lead unsubscribes | `lead_email`, `campaign_id` |34| `campaign_completed` | All leads in campaign processed | `campaign_id`, `campaign_name` |35| `account_error` | Sending account error | `email`, `error_type` |36| `lead_interested` | Lead marked interested | `lead_email`, `campaign_id` |37| `lead_not_interested` | Lead marked not interested | `lead_email`, `campaign_id` |38| `lead_meeting_booked` | Meeting booked | `lead_email`, `campaign_id` |39| `lead_meeting_completed` | Meeting completed | `lead_email` |40| `lead_closed` | Lead closed/won | `lead_email` |41| `lead_out_of_office` | OOO reply detected | `lead_email` |42| `lead_wrong_person` | Wrong person response | `lead_email` |43| `all_events` | Subscribe to everything | Varies by event |4445## Instructions4647### Step 1: Create Webhook via API4849```typescript50import { instantly } from "./src/instantly";5152async function createWebhook() {53 // Create webhook for specific events54 const webhook = await instantly<{ id: string; name: string }>("/webhooks", {55 method: "POST",56 body: JSON.stringify({57 name: "CRM Sync — Replies & Meetings",58 target_hook_url: "https://api.yourapp.com/webhooks/instantly",59 event_type: "reply_received",60 headers: {61 "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET,62 },63 }),64 });65 console.log(`Webhook created: ${webhook.id}`);6667 // Create additional webhooks for other events68 for (const event of ["lead_interested", "lead_meeting_booked", "email_bounced"]) {69 await instantly("/webhooks", {70 method: "POST",71 body: JSON.stringify({72 name: `CRM Sync — ${event}`,73 target_hook_url: "https://api.yourapp.com/webhooks/instantly",74 event_type: event,75 headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },76 }),77 });78 }7980 // Or subscribe to ALL events with one webhook81 await instantly("/webhooks", {82 method: "POST",83 body: JSON.stringify({84 name: "All Events Monitor",85 target_hook_url: "https://api.yourapp.com/webhooks/instantly/all",86 event_type: "all_events",87 headers: { "X-Webhook-Secret": process.env.INSTANTLY_WEBHOOK_SECRET },88 }),89 });90}91```9293### Step 2: Build Event Handler9495```typescript96import express from "express";9798const app = express();99app.use(express.json());100101app.post("/webhooks/instantly", async (req, res) => {102 // Validate secret103 if (req.headers["x-webhook-secret"] !== process.env.INSTANTLY_WEBHOOK_SECRET) {104 return res.status(401).json({ error: "Unauthorized" });105 }106107 // Respond 200 immediately — Instantly retries 3x in 30s on failure108 res.status(200).json({ received: true });109110 const { event_type, data } = req.body;111 console.log(`Event: ${event_type}`, JSON.stringify(data).slice(0, 300));112113 try {114 await routeEvent(event_type, data);115 } catch (err) {116 console.error(`Failed to process ${event_type}:`, err);117 }118});119120async function routeEvent(eventType: string, data: any) {121 switch (eventType) {122 case "reply_received":123 await handleReply(data);124 break;125 case "email_bounced":126 await handleBounce(data);127 break;128 case "lead_interested":129 case "lead_meeting_booked":130 case "lead_closed":131 await handlePositiveOutcome(eventType, data);132 break;133 case "lead_unsubscribed":134 await handleUnsubscribe(data);135 break;136 case "campaign_completed":137 await handleCampaignComplete(data);138 break;139 case "account_error":140 await handleAccountError(data);141 break;142 default:143 console.log(`Unhandled event: ${eventType}`);144 }145}146```147148### Step 3: Implement Event Handlers149150```typescript151async function handleReply(data: {152 lead_email: string;153 campaign_id: string;154 reply_text: string;155}) {156 console.log(`Reply from ${data.lead_email} in campaign ${data.campaign_id}`);157158 // Sync to CRM159 await crmClient.updateContact(data.lead_email, {160 status: "replied",161 lastReply: data.reply_text,162 lastActivity: new Date(),163 });164165 // Notify sales team166 await slackNotify("#sales-replies", {167 text: `Reply from ${data.lead_email}:\n${data.reply_text.slice(0, 500)}`,168 });169}170171async function handleBounce(data: {172 lead_email: string;173 bounce_type: string;174 reason: string;175}) {176 console.log(`Bounce: ${data.lead_email} (${data.bounce_type})`);177178 if (data.bounce_type === "hard") {179 // Add to global block list180 await instantly("/block-lists-entries", {181 method: "POST",182 body: JSON.stringify({ bl_value: data.lead_email }),183 });184 console.log(`Added ${data.lead_email} to block list`);185 }186}187188async function handlePositiveOutcome(189 eventType: string,190 data: { lead_email: string; campaign_id: string }191) {192 const statusMap: Record<string, string> = {193 lead_interested: "interested",194 lead_meeting_booked: "meeting_scheduled",195 lead_closed: "closed_won",196 };197198 await crmClient.updateContact(data.lead_email, {199 status: statusMap[eventType] || eventType,200 lastActivity: new Date(),201 });202203 if (eventType === "lead_meeting_booked") {204 await slackNotify("#sales-wins", {205 text: `Meeting booked with ${data.lead_email}!`,206 });207 }208}209210async function handleUnsubscribe(data: { lead_email: string }) {211 // Add to block list to prevent future outreach across all campaigns212 await instantly("/block-lists-entries", {213 method: "POST",214 body: JSON.stringify({ bl_value: data.lead_email }),215 });216 console.log(`Unsubscribed + blocked: ${data.lead_email}`);217}218219async function handleCampaignComplete(data: { campaign_id: string }) {220 // Pull final analytics221 const analytics = await instantly(`/campaigns/analytics?id=${data.campaign_id}`);222 console.log(`Campaign complete:`, analytics);223}224225async function handleAccountError(data: { email: string; error_type: string }) {226 console.error(`Account error: ${data.email} — ${data.error_type}`);227 await slackNotify("#ops-alerts", {228 text: `Instantly account error: ${data.email}\nType: ${data.error_type}`,229 });230}231```232233### Step 4: Manage Webhooks234235```typescript236// List all webhooks237async function listWebhooks() {238 const webhooks = await instantly<Array<{239 id: string; name: string; event_type: string; target_hook_url: string;240 }>>("/webhooks?limit=50");241242 for (const w of webhooks) {243 console.log(`${w.id}: ${w.name} [${w.event_type}] -> ${w.target_hook_url}`);244 }245}246247// Test a webhook248async function testWebhook(webhookId: string) {249 await instantly(`/webhooks/${webhookId}/test`, { method: "POST" });250}251252// Resume a paused webhook253async function resumeWebhook(webhookId: string) {254 await instantly(`/webhooks/${webhookId}/resume`, { method: "POST" });255}256257// Check delivery status258async function checkDeliveryHealth() {259 const summary = await instantly("/webhook-events/summary");260 console.log("Webhook delivery summary:", summary);261262 const byDate = await instantly("/webhook-events/summary-by-date");263 console.log("By date:", byDate);264}265266// Delete a webhook267async function deleteWebhook(webhookId: string) {268 await instantly(`/webhooks/${webhookId}`, { method: "DELETE" });269}270```271272## Key API Endpoints273274| Method | Path | Purpose |275|--------|------|---------|276| `POST` | `/webhooks` | Create webhook subscription |277| `GET` | `/webhooks` | List webhooks |278| `PATCH` | `/webhooks/{id}` | Update webhook |279| `DELETE` | `/webhooks/{id}` | Delete webhook |280| `POST` | `/webhooks/{id}/test` | Send test event |281| `POST` | `/webhooks/{id}/resume` | Resume paused webhook |282| `GET` | `/webhook-events` | List webhook events |283| `GET` | `/webhook-events/summary` | Delivery summary |284285## Error Handling286287| Issue | Cause | Solution |288|-------|-------|----------|289| No events delivered | Webhook not registered or paused | Check `GET /webhooks`, resume if paused |290| Duplicate events | Retry delivery | Deduplicate by event ID + timestamp |291| Webhook paused automatically | Too many delivery failures | Fix endpoint, then `POST /webhooks/{id}/resume` |292| 30s timeout | Handler takes too long | Return 200 immediately, process async |293| Missing event_type | Using custom label events | Check `custom_interest_value` field |294295## Resources296297- Instantly Webhook API298- Instantly Webhook Events299- [Instantly Blog: Webhooks Guide](https://instantly.ai/blog/api-webhooks-custom-integrations-for-outreach/)300301## Next Steps302303For performance optimization, see `instantly-performance-tuning`.304305---306307**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`