# Surge Webhooks

[Surge](https://docs.surge.app/api-reference/webhooks) is an SMS/MMS and voice messaging API for
businesses. You register one webhook endpoint per project in the dashboard, and Surge delivers events
from every account under the project as JSON `POST` requests signed with
Standard Webhooks headers: `webhook-id`, `webhook-timestamp` and
`webhook-signature`. The event name is the top-level `type` of the envelope
`{id, type, timestamp, account_id, data}`, and `data` varies by event.

## When to Use This Skill

* How do I receive Surge webhooks?
* How do I verify Surge webhook signatures (`webhook-signature`, `whsec_` secret)?
* How do I handle `message.received`, `message.delivered`, `message.failed` or `contact.opted_out` events?
* How do I react to `campaign.approved` or `phone_number.attached_to_campaign`?
* Why is my Surge webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. Surge signs it with HMAC-SHA256 using the
base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or more
space-separated `v1,<signature>` entries (several during secret rotation). Use a Standard Webhooks library
(`standardwebhooks` on npm and PyPI), which compares in constant time. Read the `webhook-*` headers, and
when `webhook-id` is absent fall back to the `svix-*` headers (`svix-id`, `svix-timestamp`,
`svix-signature`), as Hookdeck does.

```javascript
const { Webhook } = require('standardwebhooks');

const prefix = req.get('webhook-id') ? 'webhook' : 'svix';
const headers = {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
};
// rawBody: the raw Buffer/string, never re-serialized JSON
const event = new Webhook(process.env.SURGE_WEBHOOK_SECRET).verify(rawBody, headers);
// throws on a bad signature or a timestamp outside the window; returns the parsed body

```

```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["SURGE_WEBHOOK_SECRET"])  # "whsec_..."
event = wh.verify(raw_body, {"webhook-id": id_, "webhook-timestamp": ts, "webhook-signature": sig})

```

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

The secret is shown per endpoint on the dashboard's Webhooks page. It looks like
`whsec_<base64-encoded-bytes>`; keep the full value in `SURGE_WEBHOOK_SECRET`. Handlers fail closed: with
the variable unset they return 500 instead of accepting anything. A missing header or a bad signature
returns 400. The raw body is required, so in Express use `express.raw({ type: '*/*' })` and parse the body
only after the check. Earlier Surge versions sent `surge-signature`; that header is deprecated, so read
`webhook-signature`.

### Hookdeck retries and replays hit the 5-minute window

The Standard Webhooks libraries always enforce a fixed 5-minute timestamp window, which matches Surge's
recommendation to reject requests more than five minutes old, but it can't be configured. Hookdeck
retries and replays resend the **original** timestamp, so the library rejects any retry or replay that
happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old
events), use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surge-webhooks/references/verification.md), where the
window can be dropped or widened. Deduplicate on the envelope `id` instead of relying on the timestamp.

## Common Event Types

Fourteen events are documented, and the list may not be exhaustive. The name is `type` in the JSON body.
The examples handle all of these and acknowledge any other type with 200:

| Event | Triggered When |
| --- | --- |
| `message.sent` | Surge accepts an outbound message and passes it to the carrier |
| `message.delivered` | The carrier confirms delivery (not every carrier reports it) |
| `message.failed` | An outbound message could not be delivered; `failure_reason` says why |
| `message.received` | An inbound message arrives at one of your phone numbers |
| `contact.opted_in` | A contact opts back in (START, YES, UNSTOP) |
| `contact.opted_out` | A contact opts out (STOP, CANCEL, UNSUBSCRIBE, END, QUIT) |
| `conversation.created` | A new conversation thread is created (fires alongside the first `message.received`) |
| `campaign.approved` | A campaign becomes `active` after carrier approval |
| `phone_number.imported` | A number is imported from another provider (porting or hosting) |
| `phone_number.attached_to_campaign` | A number is attached to a campaign and ready for production sends |
| `link.followed` | A contact first clicks a shortened link in a message |
| `call.ended` | A call ends, whatever the direction or termination |
| `recording.completed` | A non-voicemail call recording is ready to download |
| `voicemail.received` | A voicemail recording is ready to download |

See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surge-webhooks/references/overview.md) for the envelope, payload examples and delivery
behavior.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Standard Webhooks format, starts with whsec_)
SURGE_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

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

```

Surge blocks webhook URLs that resolve to private IPs or `.local`/`.test`-style TLDs, so use the tunnel's
public URL as the endpoint, not `localhost`.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surge-webhooks/references/overview.md) - Surge webhook concepts, events, payload shape, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surge-webhooks/references/setup.md) - Add the endpoint in the dashboard and get the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/surge-webhooks/references/verification.md) - Standard Webhooks verification details, header fallback, manual verifier, timestamp window