# 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_received` or `email_sent` events?
* How do I tell `lead_interested`, `lead_meeting_booked` and 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](https://developer.instantly.ai/api-reference/webhook/create-webhook)):

```json
{ "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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/instantly-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/instantly-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/instantly-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/instantly-webhooks/references/overview.md).

## Environment Variables

```bash
# 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

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/instantly-webhooks/references/overview.md) — Event types, payload fields, custom labels, idempotency key
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/instantly-webhooks/references/setup.md) — Adding a webhook and its headers in the UI or API v2, testing, resuming disabled webhooks
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/instantly-webhooks/references/verification.md) — Header authentication in detail, gotchas, debugging failures