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() {
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}`);
for (const event of ["lead_interested", "lead_meeting_booked", "email_bounced"]) {
await instantly("/webhooks", {
method: "POST",
body: JSON.stringify({
name: ,
: ,
: event,
: { : process.. },
}),
});
}
(, {
: ,
: .({
: ,
: ,
: ,
: { : process.. },
}),
});
}
Step 2: Build Event Handler
import express from "express";
const app = express();
app.use(express.json());
app.post("/webhooks/instantly", async (req, res) => {
if (req.headers["x-webhook-secret"] !== process.env.INSTANTLY_WEBHOOK_SECRET) {
return res.status(401).json({ error: "Unauthorized" });
}
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 () {
(eventType) {
:
(data);
;
:
(data);
;
:
:
:
(eventType, data);
;
:
(data);
;
:
(data);
;
:
(data);
;
:
.();
}
}
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}`);
await crmClient.updateContact(data.lead_email, {
status: "replied",
lastReply: data.reply_text,
lastActivity: new Date(),
});
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") {
(, {
: ,
: .({ : data. }),
});
.();
}
}
() {
: <, > = {
: ,
: ,
: ,
};
crmClient.(data., {
: statusMap[eventType] || eventType,
: (),
});
(eventType === ) {
(, {
: ,
});
}
}
() {
(, {
: ,
: .({ : data. }),
});
.();
}
() {
analytics = ();
.(, analytics);
}
() {
.();
(, {
: ,
});
}
Step 4: Manage 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}`);
}
}
async function testWebhook(webhookId: string) {
await instantly(`/webhooks/${webhookId}/test`, { method: "POST" });
}
async function resumeWebhook(webhookId: string) {
await instantly(`/webhooks/${webhookId}/resume`, { method: "POST" });
}
() {
summary = ();
.(, summary);
byDate = ();
.(, byDate);
}
() {
(, { : });
}
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.