Skip to main content
You can use the x-retell-signature header together with your Retell API Key to verify the webhook comes from Retell AI, not from a malicious third party. We have provided verify function in our SDKs to help you with this.
Only the api key that has a webhook badge next to it can be used to verify the webhook.
You can also check and allowlist Retell IP addresses: 100.20.5.228. The following code snippets demonstrate how to verify and handle the webhook in Node.js and Python.

Install the SDK

Install the corresponding Python or Node.js SDK:

Sample Code

// install the sdk: https://docs.retellai.com/get-started/sdk
import { Retell } from "retell-sdk";
import express from "express";

const app = express();
// Use raw body for signature verification, not JSON.stringify(req.body).
app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  const rawBody = req.body.toString("utf-8");
  if (
    !Retell.verify(
      rawBody,
      process.env.RETELL_API_KEY,
      req.headers["x-retell-signature"],
    )
  ) {
    console.error("Invalid signature");
    return;
  }
  const {event, call} = JSON.parse(rawBody);
  // process the webhook

  // Acknowledge the receipt of the event
  res.status(204).send();
});

Verify Without SDK

If you’re using a language without an official Retell SDK, you can verify the webhook signature manually. The signature uses HMAC-SHA256.

How the Signature Works

Every webhook request includes an X-Retell-Signature header in the format:
v={timestamp},d={hex_digest}
  • v is the Unix timestamp in milliseconds when the webhook was sent.
  • d is the HMAC-SHA256 hex digest of the raw request body concatenated with the timestamp.

Verification Steps

  1. Extract the X-Retell-Signature header from the request.
  2. Parse the timestamp (v) and digest (d) from the header using the pattern v=(\d+),d=(.*).
  3. Check that the timestamp is within 5 minutes of the current time (to prevent replay attacks).
  4. Compute HMAC-SHA256(raw_body + timestamp, api_key) where + is string concatenation.
  5. Compare the computed hex digest with the d value from the header. If they match, the webhook is authentic.
You must use the raw request body string for verification, not a re-serialized version from parsed JSON. Re-serializing may change whitespace or key ordering, which will cause verification to fail.

Sample Code

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"time"
)

func verifyWebhook(rawBody string, apiKey string, signature string) bool {
	re := regexp.MustCompile(`v=(\d+),d=(.*)`)
	matches := re.FindStringSubmatch(signature)
	if len(matches) != 3 {
		return false
	}

	timestamp, err := strconv.ParseInt(matches[1], 10, 64)
	if err != nil {
		return false
	}
	digest := matches[2]

	// Check timestamp is within 5 minutes
	now := time.Now().UnixMilli()
	if math.Abs(float64(now-timestamp)) > 5*60*1000 {
		return false
	}

	// Compute HMAC-SHA256 and use constant-time comparison
	mac := hmac.New(sha256.New, []byte(apiKey))
	mac.Write([]byte(rawBody + matches[1]))
	expectedMAC, _ := hex.DecodeString(digest)

	return hmac.Equal(mac.Sum(nil), expectedMAC)
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	rawBody := string(body)
	signature := r.Header.Get("X-Retell-Signature")

	if !verifyWebhook(rawBody, os.Getenv("RETELL_API_KEY"), signature) {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Process the webhook
	fmt.Println("Webhook verified successfully")
	w.WriteHeader(http.StatusNoContent)
}

func main() {
	http.HandleFunc("/webhook", webhookHandler)
	http.ListenAndServe(":8080", nil)
}