Verification SDKs

When working with webhooks, it’s crucial to ensure that the payloads you’re receiving are genuinely from Straddle and haven’t been tampered with. Payload verification helps you verify the authenticity of incoming requests by using security headers and signature checks, making your integration more secure against malicious actors.

In this section, you’ll learn how to verify Straddle payloads step-by-step. By following this guide, you can confidently establish that your server is interacting with trusted data, allowing you to safely process events without worrying about spoofed or modified payloads.

Whether you’re new to webhooks or just need a refresher, the following instructions will provide everything you need to get started with secure webhook verification using Straddle.

Straddle has teamed up with Svix to make webhook verification as secure and straightforward as possible. This partnership means you get the best in payload authenticity and security.

Verify with SVIX Libraries

To integrate with Straddle webhooks securely, you’ll need to install the Svix libraries for your language or framework of choice. Below are installation snippets for various languages and environments.

Use the Raw Request Body

You need to use the raw request body when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. Avoid parsing the request and then re-stringifying it before verification.

The signature for each endpoint is available where you added the endpoint, e.g., the Straddle dashboard.

Verifying Payloads

Below is example verification code for various languages using Svix SDKs. Each code snippet shows how to verify the raw request body using the provided headers and secret.

Framework Specific Examples

Below are examples for how to handle raw request bodies and verification in various frameworks:

Python (Django)

from django.views.decorators.csrf import csrf_exempt
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:
        return HttpResponse(status=400)

    # Do something with the message...
    return HttpResponse(status=204)

Python (Flask)

from flask import request, Flask
from svix.webhooks import Webhook, WebhookVerificationError

app = Flask(__name__)
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

@app.route('/webhook/', methods=['POST'])
def webhook_handler():
    headers = request.headers
    payload = request.get_data()

    try:
        wh = Webhook(secret)
        msg = wh.verify(payload, headers)
    except WebhookVerificationError:
        return ('', 400)

    # Do something with the message...
    return ('', 204)

Python (FastAPI)

from fastapi import FastAPI, Request, Response, status
from svix.webhooks import Webhook, WebhookVerificationError

app = FastAPI()
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"

@app.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:
        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) {
        return 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 || "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

export async function POST(req: Request) {
  const webhook_id = req.headers.get("webhook-id") ?? "";
  const webhook_timestamp = req.headers.get("webhook-timestamp") ?? "";
  const webhook_signature = req.headers.get("webhook-signature") ?? "";

  const body = await req.text();

  const svx = new Webhook(webhookSecret);

  let msg;
  try {
    msg = svx.verify(body, {
      "webhook-id": webhook_id,
      "webhook-timestamp": webhook_timestamp,
      "webhook-signature": webhook_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) {
        return {
            statusCode: 400,
            body: JSON.stringify({})
        };
    }

    // Do something with the message...
    return {
        statusCode: 200,
        body: JSON.stringify({})
    };
};

Node.js (Express)

import { Webhook } from "svix";
import bodyParser from "body-parser";
import express from "express";

const app = express();
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);
    try {
        wh.verify(payload, headers);
    } catch (err) {
        return res.status(400).json({});
    }

    // Do something with the message...
    res.json({});
});

app.listen(3000, () => console.log('Server started on port 3000'));

Node.js (NestJS)

main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { rawBody: true });
  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;

    try {
      wh.verify(payload, headers);
      // Do something with the message...
    } catch (err) {
      // handle error
    }
  }
}

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)

use axum::{body::Bytes, http::StatusCode};
use hyper::HeaderMap;
use svix::webhooks::Webhook;

pub const SECRET: &'static str = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";

pub async fn webhook_in(headers: HeaderMap, body: Bytes) -> StatusCode {
    let wh = Webhook::new(SECRET);
    if wh.is_err() {
        return StatusCode::INTERNAL_SERVER_ERROR;
    }

    if wh.unwrap().verify(&body[..], &headers).is_err() {
        return StatusCode::BAD_REQUEST;
    }

    // Do something with the message...
    StatusCode::NO_CONTENT
}

Ruby (Ruby on Rails)

config/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

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

PHP (Laravel)

use Illuminate\Http\Request;
use Svix\Webhook;
use Svix\Exception\WebhookVerificationException;

Route::post('webhook', function(Request $request) {
    $payload = $request->getContent();
    $headers = collect($request->headers->all())->map(fn($item) => $item[0]);

    try {
        $wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw");
        $json = $wh->verify($payload, $headers->toArray());

        // Do something with the message...
        return response()->noContent();
    } catch (WebhookVerificationException $e) {
        return response(null, 400);
    }
});