> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselevers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying Webhooks

> Secure your webhook endpoint by verifying Svix signatures

## 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.

<Notice>
  Always verify webhook signatures before processing any event data. Skipping verification leaves your application vulnerable to spoofing attacks.
</Notice>

## 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          |

The `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:

1. Navigate to **Settings > Webhooks** in the Levers Dashboard
2. Click on your configured webhook endpoint
3. Copy the **Webhook Secret** (starts with `whsec_`)
4. Store this securely in your application (use environment variables)

<Warning>
  Never commit your webhook secret to version control. Always use environment variables or a secure secret management system.
</Warning>

***

## Installation

Install the Svix library for your preferred language:

<CodeGroup>
  ```bash package-install-nodejs.sh theme={null}
  npm install svix
  ```

  ```bash package-install-python.sh theme={null}
  pip install svix
  ```

  ```bash package-install-go.sh theme={null}
  go get github.com/svix/svix-webhooks/go
  ```

  ```bash package-install-ruby.sh theme={null}
  gem install svix
  ```

  ```bash package-install-php.sh theme={null}
  composer require svix/svix
  ```

  ```bash package-install-csharp.sh theme={null}
  dotnet add package Svix
  ```

  ```bash package-install-java.sh theme={null}
  # Gradle
  implementation "com.svix:svix:1.x.y"
  ```

  ```bash package-install-rust.sh theme={null}
  # Add to Cargo.toml
  svix = "1.20.0"
  ```
</CodeGroup>

<Notice>
  **Important:** Use the raw request body when verifying webhooks. The cryptographic signature is sensitive to even the slightest changes. Do not parse and re-stringify the payload.
</Notice>

***

## Basic Verification

<CodeGroup>
  ```javascript verify.js theme={null}
  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);
  ```

  ```python verify.py theme={null}
  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)
  ```

  ```go verify.go theme={null}
  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
  ```

  ```ruby verify.rb theme={null}
  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)
  ```

  ```php verify.php theme={null}
  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);
  ```

  ```java verify.java theme={null}
  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
  ```

  ```csharp verify.cs theme={null}
  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);
  ```
</CodeGroup>

***

## Framework Examples

### Node.js Frameworks

<CodeGroup>
  ```javascript express.js theme={null}
  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'));
  ```

  ```javascript nextjs.js theme={null}
  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 });
    }
  }
  ```
</CodeGroup>

### Python Frameworks

<CodeGroup>
  ```python flask.py theme={null}
  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)
  ```

  ```python fastapi.py theme={null}
  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)
  ```

  ```python django.py theme={null}
  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)
  ```
</CodeGroup>

### Go Frameworks

<CodeGroup>
  ```go gin.go theme={null}
  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()
  }
  ```

  ```go stdlib.go theme={null}
  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)
  }
  ```
</CodeGroup>

### Ruby on Rails

Add a route to `config/routes.rb`:

```ruby theme={null}
Rails.application.routes.draw do
  post "/webhook", to: "webhook#index"
end
```

Create the controller `app/controllers/webhook_controller.rb`:

```ruby theme={null}
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)

In `routes/api.php`:

```php theme={null}
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:

```bash theme={null}
# 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
```

Then use the ngrok URL (e.g., `https://abc123.ngrok.io/webhook`) in your webhook configuration.

***

<Callout title="Need Help?" href="mailto:support@uselevers.com">
  Contact our support team if you encounter issues with webhook verification.
</Callout>
