
- Using a library
- Manual verification
We are going to use the Svix open-source library to verify webhooks. First, install the relevant libraries for your language:Next, verify webhooks using the code below. The payload is the raw (string) body of the request, and the headers are the headers passed in the request.Remember to get the signature secret from the endpoint details in the console.
npm install svix
// Or
yarn add svix
pip install svix
http = "1.0.0"
svix = "1.20.0"
go get github.com/svix/svix-webhooks/go
// Gradle: Add this dependency to your project's build file:
implementation "com.svix:svix:0.x.y"
// Maven: Add this dependency to your project's POM file:
<dependency>
<groupId>com.svix</groupId>
<artifactId>svix</artifactId>
<version>0.x.y</version>
</dependency>
// Gradle: Add this dependency to your project's build file:
implementation "com.svix.kotlin:svix-kotlin:0.x.y"
// Maven: Add this dependency to your project's POM file:
<dependency>
<groupId>com.svix.kotlin</groupId>
<artifactId>svix-kotlin</artifactId>
<version>0.x.y</version>
</dependency>
gem install svix
dotnet add package Svix
composer require svix/svix
# On macOS install via Homebrew:
brew install svix/svix/svix
# On Linux install via Scoop:
scoop bucket add svix https://github.com/svix/scoop-svix.git
scoop install svix
You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. Watch out for frameworks that parse the request as JSON and then stringify it, as this will break the signature verification.See examples below for how to get the raw request body with different frameworks.
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
const headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
const payload = wh.verify(payload, headers);
from svix.webhooks import Webhook
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
# These were all sent from the server
headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
}
payload = '{"test": 2432232314}'
wh = Webhook(secret)
# Throws on error, returns the verified content on success
payload = wh.verify(payload, headers)
use svix::webhooks::Webhook;
let secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw".to_string();
let mut headers = http::HeaderMap::new();
headers.insert("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek");
headers.insert("svix-timestamp", "1614265330");
headers.insert(
"svix-signature",
"v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
);
let payload = b"{\"test\": 2432232314}";
let wh = Webhook::new(&secret)?;
wh.verify(&payload, &headers)?;
// returns Ok on success, Err otherwise
import (
svix "github.com/svix/svix-webhooks/go"
)
secret := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
// These were all sent from the server
headers := http.Header{}
headers.Set("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek")
headers.Set("svix-timestamp", "1614265330")
headers.Set("svix-signature", "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=")
payload := []byte(`{"test": 2432232314}`)
wh, err := svix.NewWebhook(secret)
err := wh.Verify(payload, headers)
// returns nil on success, error otherwise
import com.svix.Webhook;
String secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
HashMap<String, List<String>> headerMap = new HashMap<String, List<String>>();
headerMap.put("svix-id", Arrays.asList("msg_p5jXN8AQM9LWM0D4loKWxJek"));
headerMap.put("svix-timestamp", Arrays.asList("1614265330"));
headerMap.put("svix-signature", Arrays.asList("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="));
HttpHeaders headers = HttpHeaders.of(headerMap, BiPredicate<String, String>)
String payload = "{\"test\": 2432232314}";
Webhook webhook = new Webhook(secret);
webhook.verify(payload, headers)
// throws WebhookVerificationError exception on failure.
import com.svix.kotlin.Webhook
val secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
val headersMap = mapOf(
"svix-id" to listOf("msg_p5jXN8AQM9LWM0D4loKWxJek"),
"svix-timestamp" to listOf("1614265330"),
"svix-signature" to listOf("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=")
)
val headers = HttpHeaders.of(headersMap) { _, _ -> true }
val payload = "{\"test\": 2432232314}";
val webhook = Webhook(secret);
webhook.verify(payload, headers)
// throws WebhookVerificationError exception on failure.
require 'svix'
# These were all sent from the server
headers = {
"svix-id" => "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp" => "1614265330",
"svix-signature" => "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
}
payload = '{"test": 2432232314}'
wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
# Raises on error, returns the verified content on success
json = wh.verify(payload, headers)
using Svix;
using System.Net;
// These were all sent from the server
var headers = new WebHeaderCollection();
headers.Set("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek");
headers.Set("svix-timestamp", "1614265330");
headers.Set("svix-signature", "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=");
var payload = "{\"test\": 2432232314}";
var wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw/Je4ZJEGP1QFb");
// Throws on error
wh.Verify(payload, headers);
// import using composers autoload
require_once('vendor/autoload.php');
// or manually
require_once('/path/to/svix/php/init.php');
// These were all sent from the server
$payload = '{"test": 2432232314}';
$header = array(
'svix-id' => 'msg_p5jXN8AQM9LWM0D4loKWxJek',
'svix-timestamp' => '1614265330',
'svix-signature' => 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',
);
// Throws on error, returns the verified content on success
$wh = new \Svix\Webhook('whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw');
$json = $wh->verify($payload, $header);
export SVIX_AUTH_TOKEN="AUTH_TOKEN"
svix verify --secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw --msg-id msg_p5jXN8AQM9LWM0D4loKWxJek --timestamp 1614265330 --signature v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= '{"test": 2432232314}'
This is a manual verification process. We recommend using a library to verify webhooks automatically.
svix-id: The unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is resent (e.g., due to a previous failure).svix-timestamp: Timestamp in seconds since epoch.svix-signature: The Base64 encoded list of signatures (space delimited).
Constructing the Signed Content
The content to sign is composed by concatenating the ID, timestamp, and payload, separated by the full-stop character (.). In code, it will look something like this:const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
body is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying.Ensure you use the raw request body for verification. Parsing and re-stringifying JSON will alter the content and cause verification failure.
Determining the Expected Signature
We use HMAC with SHA-256 to sign webhooks.To calculate the expected signature, HMAC thesignedContent from above using the base64 portion of your signing secret (this is the part after the whsec_ prefix) as the key. For example, if your secret is whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, use MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw.For example, this is how you can calculate the signature in Node.js:const crypto = require('crypto');
const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";
// Need to base64 decode the secret
const secretBytes = Buffer.from(secret.split('_')[1], "base64");
const signature = crypto
.createHmac('sha256', secretBytes)
.update(signedContent)
.digest('base64');
console.log(signature);
svix-signature header.The svix-signature header is a list of space-delimited signatures with version identifiers. The list is typically one item but can contain multiple signatures, like this:v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=
v1,) before verifying the signature.Please note that to compare the signatures it’s recommended to use a constant-time string comparison method in order to prevent timing attacks.Verify timestamp
We include the timestamp of the attempt in thesvix-timestamp header. Compare this timestamp against your system timestamp to ensure it’s within an acceptable tolerance to prevent replay attacks.
