How to Verify Webhooks with the Svix Libraries
Verifying using libraries
First install the libraries if you haven’t already:
npm install svix
# Or
yarn add svix
Then 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.
You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. You should watch out for frameworks that parse the request as JSON and then stringify it because this too will break the signature verification.
See examples below for how to get the raw request body with different frameworks.
The signature you should get from where you added the endpoint, e.g. the application portal.
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
wh.verify(payload, headers);
Framework specific examples
Here are examples on how to adjust the above examples to your favourite framework!
Python (Django)
from django.http import HttpResponse
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@csrf_exempt
def webhook_handler(request):
headers = request.headers
payload = request.body
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return HttpResponse(status=400)
# Do something with the message...
return HttpResponse(status=204)
Python (Flask)
from flask import request
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@app.route('/webhook/')
def webhook_handler():
headers = request.headers
payload = request.get_data()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
return ('', 400)
# Do something with the message...
return ('', 204)
Python (FastAPI)
from fastapi import Request, Response, status
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
@router.post("/webhook/", status_code=status.HTTP_204_NO_CONTENT)
async def webhook_handler(request: Request, response: Response):
headers = request.headers
payload = await request.body()
try:
wh = Webhook(secret)
msg = wh.verify(payload, headers)
except WebhookVerificationError as e:
response.status_code = status.HTTP_400_BAD_REQUEST
return
# Do something with the message...
Node.js (Next.js)
import { Webhook } from "svix";
import { buffer } from "micro";
export const config = {
api: {
bodyParser: false,
},
}
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export default async function handler(req, res) {
const payload = (await buffer(req)).toString();
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
}
Node.js (Next.js 13 App Router)
import { Webhook } from "svix";
const webhookSecret: string = process.env.WEBHOOK_SECRET || "your-secret";
export async function POST(req: Request) {
const svix_id = req.headers.get("svix-id") ?? "";
const svix_timestamp = req.headers.get("svix-timestamp") ?? "";
const svix_signature = req.headers.get("svix-signature") ?? "";
const body = await req.text();
const sivx = new Webhook(webhookSecret);
let msg;
try {
msg = sivx.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
} catch (err) {
return new Response("Bad Request", { status: 400 });
}
console.log(msg);
// Rest
return new Response("OK", { status: 200 });
}
Node.js (Netlify Functions)
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
export const handler = async ({body, headers}) => {
const payload = body;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
}
Node.js (Express)
Note: When integrating this example into a larger codebase, you will have to make sure not to apply the express.json()
middleware to the webhook route, because the payload has to be passed to wh.verify
without any prior parsing.
import { Webhook } from "svix";
import bodyParser from "body-parser";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
app.post('/webhook', bodyParser.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body;
const headers = req.headers;
const wh = new Webhook(secret);
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
res.status(400).json({});
}
// Do something with the message...
res.json({});
});
Node.js (NestJS)
Initialize the application with the rawBody
flag set to true. See the NestJS docs for details.
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(
AppModule,
{ rawBody: true } // add rawBody flag
);
await app.listen(3000);
}
bootstrap();
// webhook.controller.ts
import { Controller, Post, RawBodyRequest, Req } from '@nestjs/common';
import { Request } from 'express';
import { Webhook } from 'svix';
@Controller('webhook')
class WebhookController {
@Post()
webhook(@Req() request: RawBodyRequest<Request>) {
const secret = 'whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw';
const wh = new Webhook(secret);
const payload = request.rawBody.toString('utf8');
const headers = request.headers;
let msg;
try {
msg = wh.verify(payload, headers);
} catch (err) {
// handle error
}
// Do something with the message...
}
}
Go (Standard lib)
package main
import (
"io"
"log"
"net/http"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
headers := r.Header
payload, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = wh.Verify(payload, headers)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Do something with the message...
w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", nil)
}
Go (Gin)
package main
import (
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
func main() {
wh, err := svix.NewWebhook(secret)
if err != nil {
log.Fatal(err)
}
r := gin.Default()
r.POST("/webhook", func(c *gin.Context) {
headers := c.Request.Header
payload, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err = wh.Verify(payload, headers)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Do something with the message...
c.JSON(200, gin.H{})
})
r.Run()
}
Rust (axum)
Add the webhook_in
route below to an axum router.
use axum::{body::Bytes, http::StatusCode};
use hyper::HeaderMap;
pub const SECRET: &'static str = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
pub async fn webhook_in(headers: HeaderMap, body: Bytes) -> StatusCode {
let wh = svix::webhooks::Webhook::new(SECRET);
if let Err(_) = wh {
return StatusCode::INTERNAL_SERVER_ERROR;
}
if let Err(_) = wh.verify(&body, &headers) {
return StatusCode::BAD_REQUEST;
}
// Do something with the message...
StatusCode::NO_CONTENT
}
Ruby (Ruby on Rails)
Once you’ve set up your project add a route to your config/routes.rb
file at the top of the Rails.application.routes.draw
block:
Rails.application.routes.draw do
post "/webhook", to: "webhook#index"
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
The route above declares that POST /webhook
requests are mapped to the index action of WebhookController
.
To create WebhookController
and its index action, we’ll run the controller generator (with the --skip-routes
option because we already have an appropriate route):
bin/rails generate controller Webhook index --skip-routes
Rails will create several files for you:
create app/controllers/webhook_controller.rb
invoke erb
create app/views/webhook
create app/views/webhook/index.html.erb
invoke test_unit
create test/controllers/webhook_controller_test.rb
invoke helper
create app/helpers/webhook_helper.rb
invoke test_unit
invoke assets
invoke scss
create app/assets/stylesheets/webhook.scss
Now we can add our verification logic to the newly created app/controllers/webhook_controller.rb
file:
require 'svix'
class WebhookController < ApplicationController
protect_from_forgery with: :null_session # disables CSRF middleware; required for API endpoints
def index begin payload = request.body.read headers = request.headers wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
json = wh.verify(payload, headers)
# Do something with the message...
head :no_content rescue head :bad_request end endend
PHP (Laravel)
In your routes/api.php
file add the following after the last use directive:
use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;
Route::post('webhook', function(Request $request) {
$payload = $request->getContent();
$headers = collect($request->headers->all())->transform(function ($item) {
return $item[0];
});
try {
$wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
$json = $wh->verify($payload, $headers);
# Do something with the message...
return response()->noContent();
} catch (WebhookVerificationException $e) {
return response(null, 400);
}
});