# Outreach Webhooks

## When to Use This Skill

* How do I receive Outreach webhooks?
* How do I verify the `Outreach-Webhook-Signature` header?
* Why does my Outreach webhook have no signature header?
* How do I handle `prospect.created`, `sequenceState.created` or `call.created` events?
* Why is my Outreach webhook signature verification failing?

## Set a Secret or Nothing Is Signed

Outreach signs a delivery **only if the webhook has a `secret` attribute**. Without one there is no `Outreach-Webhook-Signature` header, and a verifier (including Hookdeck's Outreach source) has nothing to check and rejects every request. Outreach's docs create webhooks through the API (`POST /api/v2/webhooks`), so **include `secret` in the create request** (or `PATCH` it onto an existing webhook). See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outreach-webhooks/references/setup.md).

The signature is the **bare lowercase hex** HMAC-SHA256 of the **raw request body**, keyed with the secret. There is no timestamp, no prefix, and no base64.

### Verification (core)

```javascript
const crypto = require('crypto');

function verifyOutreachSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase());
  const expectedBuf = Buffer.from(expected);
  // timingSafeEqual throws on length mismatch, so check length first
  return received.length === expectedBuf.length && crypto.timingSafeEqual(received, expectedBuf);
}
// rawBody: Buffer from express.raw({ type: '*/*' }); header: Outreach-Webhook-Signature

```

```python
import hashlib, hmac

def verify_outreach_signature(raw_body: bytes, signature_header, secret) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.strip().lower().encode(), expected.encode())

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outreach-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outreach-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/outreach-webhooks/examples/fastapi/).

Read the body with `express.raw({ type: '*/*' })` so any content type (the payload is JSON:API-styled, so it may arrive as `application/vnd.api+json`) reaches the verifier as unparsed bytes.

## Common Event Types

`meta.eventName` is `resource.action`. Resources include `account`, `call`, `emailAddress`, `mailing`, `opportunity`, `opportunityProspectRole`, `prospect`, `sequence`, `sequenceState`, `task`, `user`, `import` and `kaiaRecording`.

| Event | Triggered When |
| --- | --- |
| `prospect.created` | A prospect is created |
| `prospect.updated` | A prospect changes |
| `sequenceState.created` | A prospect is added to a sequence |
| `sequenceState.finished` | A prospect finishes a sequence |
| `call.created` | A call is logged |
| `task.completed` | A task is completed |
| `mailing.replied` | A prospect replies to an email |

The payload is JSON:API: `data` (`type`, `id`, `attributes`, `relationships`) and `meta` (`deliveredAt`, `eventName`). Create and update events carry only the changed attributes. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outreach-webhooks/references/overview.md).

## Important Details

| Header | Description |
| --- | --- |
| `Outreach-Webhook-Signature` | Hex HMAC-SHA256 of the raw body (only present when a secret is set) |
| `outreach-webhook-cleanup-token` | Token for removing the webhook through `POST /api/v2/webhooks/cleanup` |

* Respond `200` within **5 seconds**. Outreach does **not retry on any HTTP status** (including `500` and `429`); it retries only network failures (4 attempts, 1 second apart). Hand off slow work to a queue.
* Redirects are not followed.
* Outreach publishes no fixed IP list, so the signature is your authentication.

## Environment Variables

```bash
OUTREACH_WEBHOOK_SECRET=your_webhook_secret   # the secret you set on the webhook via the API

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 outreach --path /webhooks/outreach

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outreach-webhooks/references/overview.md) - What Outreach webhooks are, events and payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outreach-webhooks/references/setup.md) - Create a webhook with a secret through the API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/outreach-webhooks/references/verification.md) - Signature verification details and gotchas