> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surge.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Receiving Messages & Webhooks

> Configure a webhook endpoint, verify Surge webhook signatures, and handle incoming events in Python, TypeScript, Ruby, and Elixir.

Surge sends HTTP POST requests to a URL you control whenever something happens: a message arrives, a campaign is approved, a contact opts out. You configure the URL in the dashboard and Surge takes care of delivery, retries, and ordering.

## 1. Configure your webhook endpoint

Go to **Settings → Webhooks** in the dashboard at `hq.surge.app`. Add the URL where you want to receive events. Surge sends all event types to a single endpoint (you filter by `type` in your handler).

Your endpoint must:

* Be publicly accessible over `https://` (or `http://`, but HTTPS is strongly recommended)
* Resolve to a public IP address
* Return a `2xx` status code within the request timeout
* Handle duplicate deliveries (Surge retries failed attempts, so your handler should be idempotent)

<Warning>
  **Webhook URL restrictions.** Surge blocks webhook URLs that resolve to private IP ranges (`10.x.x.x`, `172.16.x.x`, `192.168.x.x`, `127.x.x.x`) or reserved TLDs (`.local`, `.internal`, `.localhost`, `.test`) to prevent server-side request forgery. This means `http://localhost:4000/webhooks` won't work — use a tunneling service like ngrok or Cloudflare Tunnel in development.
</Warning>

## 2. Verify the signature

Every webhook request includes a `webhook-signature` header. Verifying it confirms the request came from Surge and wasn't tampered with in transit.

Surge signs requests using HMAC-SHA256 following the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Use the `standardwebhooks` library in Python, TypeScript, or Ruby, or compute the signature directly in any language.

<Note>
  Surge sets three headers on every request: `webhook-id`, `webhook-timestamp`, and `webhook-signature`. The library handles them for you in Python / TypeScript / Ruby; you read them directly when computing the signature manually (Elixir example below).
</Note>

<CodeGroup>
  ```python Python theme={null}
  from standardwebhooks import Webhook

  wh = Webhook(os.environ["SURGE_WEBHOOK_SECRET"])

  try:
      payload = wh.verify(request.body, request.headers)
      # payload is the parsed event dict
  except Exception:
      return Response(status=400)
  ```

  ```typescript TypeScript theme={null}
  import { Webhook } from "standardwebhooks";

  const wh = new Webhook(process.env.SURGE_WEBHOOK_SECRET!);

  try {
    const payload = wh.verify(rawBody, req.headers);
    // payload is the parsed event object
  } catch {
    res.status(400).end();
    return;
  }
  ```

  ```ruby Ruby theme={null}
  require "standardwebhooks"

  wh = StandardWebhooks::Webhook.new(ENV["SURGE_WEBHOOK_SECRET"])

  begin
    payload = wh.verify(request.raw_post, request.headers)
  rescue StandardWebhooks::WebhookVerificationError
    head :bad_request and return
  end
  ```

  ```elixir Elixir theme={null}
  defp verify_surge_signature(conn, secret) do
    webhook_id = get_req_header(conn, "webhook-id") |> List.first()
    timestamp = get_req_header(conn, "webhook-timestamp") |> List.first()
    signature = get_req_header(conn, "webhook-signature") |> List.first()
    body = conn.assigns[:raw_body]

    # Secret has a "whsec_" prefix; strip it and decode the base64 payload
    "whsec_" <> encoded = secret
    decoded_secret = Base.decode64!(encoded, padding: false)

    # Standard Webhooks payload format: "{webhook-id}.{webhook-timestamp}.{body}"
    payload = "#{webhook_id}.#{timestamp}.#{body}"
    expected = :crypto.mac(:hmac, :sha256, decoded_secret, payload) |> Base.encode64()

    Plug.Crypto.secure_compare(signature, "v1,#{expected}")
  end
  ```
</CodeGroup>

### Where to find your webhook secret

Your signing secret is in **Settings → Webhooks** in the dashboard, next to your webhook endpoint URL.

## 3. Handle events

A webhook event has this shape:

```json theme={null}
{
  "type": "message.received",
  "data": {
    "id": "msg_01jav96823f9x9054d6gyzpp16",
    "blast_id": null,
    "body": "Hello there",
    "metadata": {},
    "attachments": [],
    "received_at": "2024-10-21T23:29:43Z",
    "conversation": {
      "id": "cnv_01jav8xy7fe4nsay3c9deqxge9",
      "phone_number": {
        "id": "pn_01jsjwe4d9fx3tpymgtg958d9w",
        "number": "+18015556789",
        "type": "local"
      },
      "contact": {
        "id": "ctc_01ja88cboqffhswjx8zbak3ykk",
        "phone_number": "+18015551234"
      }
    }
  }
}
```

A minimal handler checks `type` and dispatches:

```python theme={null}
@app.post("/webhooks/surge")
async def surge_webhook(request: Request):
    payload = wh.verify(await request.body(), request.headers)

    match payload["type"]:
        case "message.received":
            await handle_inbound_message(payload["data"])
        case "contact.opted_out":
            await handle_opt_out(payload["data"])
        case _:
            pass  # ignore events you don't need

    return Response(status_code=200)
```

<Tip>
  Return `200` quickly. If your processing takes time, enqueue the work and return immediately — Surge will retry if your handler times out.
</Tip>

## Replay protection

<Note>
  The Standard Webhooks spec includes a timestamp in the signature payload. Surge rejects replays outside a tolerance window. If you're testing with recorded payloads, make sure your test framework handles timestamp validation or disables it appropriately.
</Note>

## Older signature header

<Warning>
  Earlier versions of Surge used `surge-signature` as the header name. This header is deprecated and being phased out. If your handler still reads `surge-signature`, migrate to `webhook-signature`. See [Deprecation Notices](../resources/deprecations).
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook events" icon="bell" href="./events">
    Every event type, its payload shape, and when it fires.
  </Card>

  <Card title="Two-way messaging" icon="comments" href="./two-way">
    Reply to inbound messages and keep conversation state in sync.
  </Card>

  <Card title="Signature validation" icon="shield-check" href="../api-reference/webhooks/signature-validation">
    Reference for the Standard Webhooks signature scheme Surge uses.
  </Card>

  <Card title="Deprecation notices" icon="clock" href="../resources/deprecations">
    Migrating from the older `surge-signature` header.
  </Card>
</CardGroup>
