Agent skill

Baseten Webhooks Skill

Receive and verify Baseten webhooks. Use when setting up Baseten async inference callbacks (webhook_endpoint on /async_predict), debugging X-BASETEN-SIGNATURE verification, or handling async_request_completed events.

Install this skill

npx skills add hookdeck/webhook-skills --skill baseten-webhooks


When to Use This Skill

  • How do I receive Baseten webhooks for async inference results?
  • How do I verify the X-BASETEN-SIGNATURE header?
  • How do I handle async_request_completed events, including failed requests?
  • Why does my Baseten webhook signature verification fail (raw body, v1= prefix)?
  • How do I avoid losing model output when Baseten only retries once?

Setup note

Create a webhook secret in the Baseten Secrets tab and paste it here. Without one, deliveries are unsigned and fail verification.

Store it as BASETEN_WEBHOOK_SECRET. Baseten calls your endpoint only for async requests that set webhook_endpoint on /async_predict.

Verification (core)

Baseten sends X-BASETEN-SIGNATURE: v1=<hex>: a lowercase hex HMAC-SHA256 of the raw request body bytes, keyed with the UTF-8 bytes of your webhook secret. Nothing else is signed (no timestamp or id), so there is no replay window to check. During secret rotation the header may hold several comma-separated values; accept if any matches. Validate the hex (64 characters) before decoding, then compare in constant time.

Node:

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  if (!header || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return header.split(',').some((part) => {
    const sig = part.trim().replace(/^v1=/, '');
    if (!/^[0-9a-fA-F]{64}$/.test(sig)) return false;
    return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected);
  });
}

Python:

import hashlib, hmac, re

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    if not header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    for part in header.split(","):
        sig = part.strip().removeprefix("v1=")
        if re.fullmatch(r"[0-9a-fA-F]{64}", sig) and hmac.compare_digest(bytes.fromhex(sig), expected):
            return True
    return False

Read the body as bytes before any JSON parsing (Express: express.raw({ type: '*/*' }), Next.js: await request.arrayBuffer(), FastAPI: await request.body()). Missing, malformed or mismatched signature: respond 401.

For complete handlers with route wiring, event dispatch, and tests, see:

Event Types

Baseten documents one event type. There is no type header; dispatch on the body's type field.

typeDescription
async_request_completedAsync inference finished. Success and failure share this type: data holds the output (or null) and errors is empty on success or holds error objects (e.g. MODEL_PREDICT_ERROR) on failure

Log and acknowledge any other type with 200.

Important Headers

HeaderDescription
X-BASETEN-SIGNATUREv1=<hex> HMAC-SHA256 of the raw body; only sent if a webhook secret is configured
X-BASETEN-REQUEST-IDThe async request ID, for correlating with your /async_predict call

Delivery Behaviour

Best effort: 2 attempts in total (one retry about 2 s later), 10 s timeout per attempt, retried only on 500, 502, 503 and 504. If both fail, the model output is lost and the request status becomes WEBHOOK_FAILED. Respond quickly, return 401 for bad signatures (not retried) and 500 only for transient failures. Persist outputs in your model's postprocess() too.

Environment Variables

BASETEN_WEBHOOK_SECRET=your_webhook_secret   # Created in the Baseten Secrets tab

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 baseten --path /webhooks/baseten

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →