Overview
Levers uses Svix to securely deliver webhooks. Every webhook request includes cryptographic signatures that you can verify to ensure the request originated from Levers and wasn’t tampered with.Signature Headers
Each webhook POST request includes the following headers:| Header | Description |
|---|---|
svix-id | Unique identifier for this webhook delivery |
svix-timestamp | Unix timestamp when the webhook was sent |
svix-signature | Comma-delimited list of signatures |
svix-signature header contains one or more signatures in the format:
svix-signature: t=timestamp,v1=signature1,v1=signature2,...
Getting Your Webhook Secret
Each webhook endpoint in Levers has its own unique secret key. To find it:- Navigate to Settings > Webhooks in the Levers Dashboard
- Click on your configured webhook endpoint
- Copy the Webhook Secret (starts with
whsec_) - Store this securely in your application (use environment variables)
Never commit your webhook secret to version control. Always use environment variables or a secure secret management system.
Installation
Install the Svix library for your preferred language:npm install svix
pip install svix
go get github.com/svix/svix-webhooks/go
gem install svix
composer require svix/svix
dotnet add package Svix
# Gradle
implementation "com.svix:svix:1.x.y"
# Add to Cargo.toml
svix = "1.20.0"
Basic Verification
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These are sent from Levers in the request headers and body
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 event = wh.verify(payload, headers);
from svix.webhooks import Webhook
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
# These are sent from Levers in the request headers and body
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
event = wh.verify(payload, headers)
package main
import (
svix "github.com/svix/svix-webhooks/go"
)
secret := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
// These are sent from Levers in the request headers and body
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
require 'svix'
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
# These are sent from Levers in the request headers and body
headers = {
"svix-id" => "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp" => "1614265330",
"svix-signature" => "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
}
payload = '{"test": 2432232314}'
wh = Svix::Webhook.new(secret)
# Raises on error, returns the verified content on success
event = wh.verify(payload, headers)
use Svix\Webhook;
$payload = '{"test": 2432232314}';
$headers = [
'svix-id' => 'msg_p5jXN8AQM9LWM0D4loKWxJek',
'svix-timestamp' => '1614265330',
'svix-signature' => 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',
];
$wh = new \Svix\Webhook('whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw');
// Throws on error, returns the verified content on success
$event = $wh->verify($payload, $headers);
import com.svix.Webhook;
import java.util.*;
String secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
HashMap<String, List<String>> headerMap = new HashMap<>();
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, (k, v) -> true);
String payload = "{\"test\": 2432232314}";
Webhook webhook = new Webhook(secret);
webhook.verify(payload, headers);
// throws WebhookVerificationError exception on failure
using Svix;
using System.Net;
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");
// Throws on error
wh.Verify(payload, headers);
Framework Examples
Node.js Frameworks
import { Webhook } from "svix";
import express from 'express';
const app = express();
const secret = 'whsec_YOUR_WEBHOOK_SECRET_HERE';
// Raw body parser - IMPORTANT: do not use express.json() for webhooks
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const payload = req.body;
const headers = req.headers;
const wh = new Webhook(secret);
try {
const event = wh.verify(payload.toString(), {
'svix-id': headers['svix-id'],
'svix-timestamp': headers['svix-timestamp'],
'svix-signature': headers['svix-signature']
});
// Process the verified event
const eventType = event.type;
const eventData = event.data;
if (eventType === 'phone_call.ended') {
console.log(`Call ended: ${eventData.providerCallId}`);
// Your business logic here
}
res.status(200).json({ status: 'success' });
} catch (err) {
console.error('Webhook verification failed:', err);
res.status(401).json({ error: 'Invalid signature' });
}
});
app.listen(3000, () => console.log('Webhook server running on port 3000'));
import { Webhook } from "svix";
const webhookSecret = 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 wh = new Webhook(webhookSecret);
try {
const event = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
});
// Process the event...
const eventType = event.type;
const eventData = event.data;
if (eventType === 'phone_call.ended') {
console.log(`Call ended: ${eventData.providerCallId}`);
}
return new Response("OK", { status: 200 });
} catch (err) {
return new Response("Bad Request", { status: 400 });
}
}
Python Frameworks
from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
@app.route('/webhook', methods=['POST'])
def webhook_handler():
headers = request.headers
payload = request.get_data() # Get raw body
try:
wh = Webhook(secret)
event = wh.verify(payload, headers)
except WebhookVerificationError as e:
return ('Invalid signature', 400)
# Process the verified event
event_type = event.get('type')
event_data = event.get('data')
if event_type == 'phone_call.ended':
print(f"Call ended: {event_data['providerCallId']}")
# Your business logic here
return ('Success', 204)
if __name__ == '__main__':
app.run(port=3000)
from fastapi import Request, Response, status
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
@router.post("/webhook")
async def webhook_handler(request: Request):
headers = request.headers
payload = await request.body() # Get raw body
try:
wh = Webhook(secret)
event = wh.verify(payload, headers)
except WebhookVerificationError as e:
return Response(status_code=status.HTTP_400_BAD_REQUEST)
# Process the verified event
event_type = event.get('type')
event_data = event.get('data')
if event_type == 'phone_call.ended':
print(f"Call ended: {event_data['providerCallId']}")
# Your business logic here
return Response(status_code=status.HTTP_204_NO_CONTENT)
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from svix.webhooks import Webhook, WebhookVerificationError
secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
@csrf_exempt
def webhook_handler(request):
headers = request.headers
payload = request.body
try:
wh = Webhook(secret)
event = wh.verify(payload, headers)
except WebhookVerificationError as e:
return HttpResponse(status=400)
# Process the verified event
event_type = event.get('type')
event_data = event.get('data')
if event_type == 'phone_call.ended':
print(f"Call ended: {event_data['providerCallId']}")
# Your business logic here
return HttpResponse(status=204)
Go Frameworks
package main
import (
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
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": "Invalid signature"})
return
}
// Process the verified event...
c.JSON(200, gin.H{"status": "success"})
})
r.Run()
}
package main
import (
"io"
"log"
"net/http"
svix "github.com/svix/svix-webhooks/go"
)
const secret = "whsec_YOUR_WEBHOOK_SECRET_HERE"
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
}
// Process the verified event...
w.WriteHeader(http.StatusNoContent)
})
http.ListenAndServe(":8080", nil)
}
Ruby on Rails
Add a route toconfig/routes.rb:
Rails.application.routes.draw do
post "/webhook", to: "webhook#index"
end
app/controllers/webhook_controller.rb:
require 'svix'
class WebhookController < ApplicationController
protect_from_forgery with: :null_session # Disable CSRF for API endpoints
def index
begin
payload = request.body.read
headers = request.headers
wh = Svix::Webhook.new("whsec_YOUR_WEBHOOK_SECRET_HERE")
event = wh.verify(payload, headers)
# Process the verified event
event_type = event['type']
event_data = event['data']
if event_type == 'phone_call.ended'
puts "Call ended: #{event_data['providerCallId']}"
# Your business logic here
end
head :no_content
rescue
head :bad_request
end
end
end
PHP (Laravel)
Inroutes/api.php:
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_YOUR_WEBHOOK_SECRET_HERE");
$event = $wh->verify($payload, $headers);
// Process the verified event
$eventType = $event['type'];
$eventData = $event['data'];
if ($eventType === 'phone_call.ended') {
Log::info("Call ended: " . $eventData['providerCallId']);
// Your business logic here
}
return response()->noContent();
} catch (WebhookVerificationException $e) {
return response(null, 400);
}
});
Testing Webhooks Locally
To test webhooks during development, use a tool like ngrok or localtunnel to expose your local server to the internet:# Install ngrok
brew install ngrok # macOS
# or visit: https://ngrok.com/download
# Start your local server
python app.py # Your webhook server
# In another terminal, expose it
ngrok http 3000
https://abc123.ngrok.io/webhook) in your webhook configuration.
Contact our support team if you encounter issues with webhook verification.

