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-SIGNATUREheader? - How do I handle
async_request_completedevents, 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.
type | Description |
|---|---|
async_request_completed | Async 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
| Header | Description |
|---|---|
X-BASETEN-SIGNATURE | v1=<hex> HMAC-SHA256 of the raw body; only sent if a webhook secret is configured |
X-BASETEN-REQUEST-ID | The 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
- references/overview.md - Baseten webhook concepts, payload, delivery
- references/setup.md - Secret and
webhook_endpointconfiguration - references/verification.md - Signature verification details and gotchas