Vendo SDKs
Concepts

Webhooks

Verify HMAC-SHA256 signatures and guard against replay attacks.

Webhook verification works in both OSS and Vendo mode because HMAC-SHA256 verification is performed locally — no Vendo backend call needed.

Setup

Set the webhook secret in your environment. You can find it in the Vendo dashboard under Deployments → Webhooks:

VENDO_WEBHOOK_SECRET=whsec_...

Verifying a webhook

Pass the raw request headers and raw body string to webhooks.verify(). It raises ValidationError on a bad signature or a stale timestamp (older than 5 minutes by default).

from fastapi import FastAPI, Request, HTTPException
import vendo
from vendo.errors import ValidationError

app = FastAPI()

@app.post("/webhooks/vendo")
async def webhook(request: Request):
    body = await request.body()
    try:
        event = vendo.webhooks.verify(
            headers=dict(request.headers),
            body=body.decode()
        )
    except ValidationError as e:
        raise HTTPException(status_code=400, detail=str(e))

    print(f"Event: {event.type}, ID: {event.id}")
    return {"ok": True}

Configure maximum age (default 300 seconds):

event = vendo.webhooks.verify(
    headers=headers,
    body=body,
    max_age_seconds=600
)

Pass the request headers and raw body string. Throws ValidationError on failure.

import express from "express";
import { Vendo } from "@vendodev/sdk";

const app = express();
const vendo = new Vendo();

// IMPORTANT: use express.raw() — not express.json() — to preserve the raw body
app.post(
  "/webhooks/vendo",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = vendo.webhooks.verify(
        req.headers as Record<string, string>,
        req.body.toString()
      );
      console.log(`Event: ${event.type}, ID: ${event.id}`);
      res.json({ ok: true });
    } catch (e) {
      res.status(400).json({ error: String(e) });
    }
  }
);

Use WebhooksAPI directly, or access it via vendo.webhooks. Pass headers as [String: String] and the raw body as a String.

import Vendo
import Vapor

func webhookHandler(_ req: Request) async throws -> Response {
    guard let body = req.body.string else {
        throw Abort(.badRequest)
    }
    let headers = Dictionary(
        uniqueKeysWithValues: req.headers.map { ($0.name.description, $0.value) }
    )

    do {
        let event = try vendo.webhooks.verify(
            headers: headers,
            body: body
        )
        print("Event:", event.type, "ID:", event.id)
        return Response(status: .ok)
    } catch VendoError.validation(let message) {
        throw Abort(.badRequest, reason: message)
    }
}

Configure max age:

let event = try vendo.webhooks.verify(
    headers: headers,
    body: body,
    maxAgeSeconds: 600
)

Webhook event shape

After a successful verify() call you receive a WebhookEvent with:

FieldTypeDescription
idstringUnique event ID (use for deduplication)
typestringEvent type, e.g. connection.connected
timestampstringISO 8601 timestamp
dataobjectEvent-specific payload

Replay protection

verify() checks that the event timestamp is within max_age_seconds of the current time (default: 5 minutes). Events older than this window are rejected with ValidationError, making replay attacks impractical.

For idempotent processing, store and check the event.id in your database before processing.

Signature algorithm

Vendo signs webhooks with HMAC-SHA256 over the raw body and sends the signature in the Vendo-Signature header alongside a Vendo-Timestamp header. The SDK verifies both.

On this page