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
- Extract the
X-Retell-Signature header from the request.
- Parse the timestamp (
v) and digest (d) from the header using the pattern v=(\d+),d=(.*).
- Check that the timestamp is within 5 minutes of the current time (to prevent replay attacks).
- Compute
HMAC-SHA256(raw_body + timestamp, api_key) where + is string concatenation.
- 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)
}