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

# Verify webhook signatures

> Verify that a webhook delivery actually came from Get2Dial before trusting or acting on its payload.

Every webhook delivery is signed. Verify the signature before acting on a payload — an
unverified webhook endpoint can be called by anyone who finds its URL.

## How it works

Each delivery carries an `X-Get2Dial-Signature` header in the form:

```
t=1735849800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

`t` is the Unix timestamp the request was signed at. `v1` is an HMAC-SHA256 signature, hex
encoded, computed over `<timestamp>.<raw request body>`, keyed with your webhook's signing
secret.

<Warning>
  Get2Dial does not enforce a timestamp tolerance window on its side. If you need replay
  protection, check that `t` is recent yourself and reject deliveries outside the window you
  choose — this is on you to implement, not something the signature check does for you.
</Warning>

## Steps

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verifyGet2DialSignature(rawBody, signatureHeader, signingSecret) {
    const parts = Object.fromEntries(
      signatureHeader.split(",").map((p) => p.split("="))
    );
    const signedPayload = `${parts.t}.${rawBody}`;
    const expected = crypto
      .createHmac("sha256", signingSecret)
      .update(signedPayload)
      .digest("hex");

    const a = Buffer.from(expected);
    const b = Buffer.from(parts.v1);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_get2dial_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
      parts = dict(p.split("=", 1) for p in signature_header.split(","))
      signed_payload = f"{parts['t']}.{raw_body.decode()}"
      expected = hmac.new(
          signing_secret.encode(), signed_payload.encode(), hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, parts["v1"])
  ```
</CodeGroup>

Always compare signatures in constant time — both examples above do this
(`crypto.timingSafeEqual`, `hmac.compare_digest`) rather than `===` or `==`, which leaks
timing information an attacker can use to guess the signature byte by byte.

## Verify

<Check>
  Send yourself a test event and confirm your verification function returns `true` for a
  genuine delivery and `false` for a payload you've tampered with.
</Check>

## Common problems

* **Verification always fails.** Confirm you're signing the **raw** request body — not a
  re-serialized version of the parsed JSON, which can differ in whitespace or key order and
  produce a different signature.
* **You need replay protection.** Implement your own timestamp-freshness check against `t`
  — it isn't enforced by Get2Dial.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks overview" href="/developers/webhooks/overview" />

  <Card title="Event catalog" href="/developers/webhooks/event-catalog" />
</CardGroup>
