VendoVendo Docs
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:

FieldPythonTypeScriptDescription
idid: strid: stringUnique event ID (use for deduplication)
typetype: strtype: stringEvent type, e.g. connection.connected
occurred_at / occurredAtoccurred_at: stroccurredAt: stringISO 8601 timestamp the event was emitted
datadata: dictdata: Record<string, unknown>Event-specific payload

Replay protection

verify() checks that the Vendo-Timestamp header is within max_age_seconds of the current time (default: 5 minutes / 300s). Events older than this window are rejected with ValidationError, making replay attacks impractical. The timestamp is accepted either as unix epoch seconds or RFC-3339, so signature failures on non-numeric Vendo-Timestamp values usually mean a malformed string rather than an algorithm mismatch.

For idempotent processing, store and check the event.id in your database before processing. Webhook delivery is at-least-once; a unique constraint on the stored id is the recommended dedup pattern.

Signature algorithm

Vendo signs webhooks with HMAC-SHA256 over "{Vendo-Timestamp}.{body}" (Stripe-style) and sends the digest in the Vendo-Signature header. The SDK verifies both the digest and the timestamp window.

On this page