Agent skill
Instantly Webhooks Skill
Receive and authenticate Instantly (Instantly.ai cold email) webhooks. Use when setting up an Instantly webhook handler, securing it with the custom headers Instantly lets you attach (e.g. Authorization: Basic ... or a secret header), debugging why the header check fails, or handling campaign events like email_sent, email_opened, reply_received, email_bounced, lead_interested, lead_unsubscribed and lead_meeting_booked. Instantly does not sign webhooks: there is no signature header or HMAC.
Install this skill
npx skills add hookdeck/webhook-skills --skill instantly-webhooks
Instantly is a cold email outreach platform. On the Hyper Growth plan and above it sends an HTTP POST with a JSON body to your URL whenever a campaign event happens: an email is sent, opened or bounces, a lead replies, or a lead's status changes.
When to Use This Skill
- How do I receive Instantly webhooks?
- How do I verify / authenticate an Instantly webhook?
- Why is my Instantly webhook header check failing?
- How do I handle
reply_receivedoremail_sentevents? - How do I tell
lead_interested,lead_meeting_bookedand custom label events apart? - What fields does an Instantly webhook payload contain?
No Signature: Custom Header Only
Instantly does not sign webhook payloads. There is no signature header, no HMAC and no timestamp header. Instead, each webhook can carry custom HTTP headers that you set yourself, in the UI ("Add headers (optional)") or in the API v2 headers object (Create webhook):
{ "target_hook_url": "https://example.com/webhooks/instantly",
"event_type": "all_events",
"headers": { "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" } }
Pick one header (Authorization with a Basic credential, or a header such as X-Instantly-Secret), put a long random value in it, and compare the whole value in constant time on every request. Without a header, anyone who knows the URL can post fake events.
Verification (core)
const crypto = require('crypto');
// The header you configured in Instantly, and its exact value.
const HEADER = (process.env.INSTANTLY_WEBHOOK_HEADER || 'authorization').toLowerCase();
const EXPECTED = process.env.INSTANTLY_WEBHOOK_SECRET; // e.g. "Basic dXNlcjpwYXNz"
function verifyInstantlyHeader(received, expected) {
if (!received || !expected) return false; // fail closed
const a = Buffer.from(String(received), 'utf8'); // the WHOLE header value
const b = Buffer.from(String(expected), 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b); // length guard first
}
// In the handler: verifyInstantlyHeader(req.headers[HEADER], EXPECTED)
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Instantly has no SDK with a webhook verification helper, so every example does this compare directly (crypto.timingSafeEqual in Node, hmac.compare_digest in Python). Authenticate before you parse the JSON body.
Common Event Types
The event name is the payload's event_type field.
| Event | Triggered when |
|---|---|
email_sent | A campaign email was sent |
email_opened | A lead opened an email |
email_link_clicked | A lead clicked a tracked link (the events guide calls it link_clicked; handle both) |
reply_received | A lead replied |
auto_reply_received | An auto-reply was received |
email_bounced | An email bounced |
lead_unsubscribed | A lead unsubscribed |
campaign_completed | A campaign completed |
account_error | A sending account hit an error |
lead_interested / lead_not_interested / lead_neutral | Lead status set |
lead_meeting_booked / lead_meeting_completed / lead_no_show | Meeting status set |
lead_closed / lead_out_of_office / lead_wrong_person | Lead status set |
| custom label name | Any custom label in your workspace, sent as event_type as-is |
Every payload has timestamp, event_type, workspace, campaign_id and campaign_name; lead events add lead_email, email_account, step, variant, and email/reply fields. There's no event ID, so build an idempotency key from event_type + timestamp + lead_email + campaign_id. See references/overview.md.
Environment Variables
# Header name you added in Instantly (case-insensitive). Default: Authorization
INSTANTLY_WEBHOOK_HEADER=Authorization
# The exact header value you added in Instantly
INSTANTLY_WEBHOOK_SECRET=Basic dXNlcm5hbWU6cGFzc3dvcmQ=
With no INSTANTLY_WEBHOOK_SECRET, the examples reject every request with a 500 rather than accept unauthenticated webhooks.
Local Development
npx hookdeck-cli listen 3000 instantly --path /webhooks/instantly
No account required — the CLI creates a guest account on first run and gives you a public HTTPS URL plus a web UI for inspecting requests (including your custom header). Use port 8000 for the FastAPI example.
Reference Materials
- references/overview.md — Event types, payload fields, custom labels, idempotency key
- references/setup.md — Adding a webhook and its headers in the UI or API v2, testing, resuming disabled webhooks
- references/verification.md — Header authentication in detail, gotchas, debugging failures