Agent skill
Daytona Webhooks Skill
Receive and verify Daytona webhooks (sandbox infrastructure for AI code). Use when setting up Daytona webhook handlers, debugging Svix signature verification (svix-id, svix-timestamp, svix-signature, whsec_ secret), or handling events like sandbox.created, sandbox.state.updated, snapshot.created, snapshot.state.updated, snapshot.removed, volume.created and volume.state.updated.
Install this skill
npx skills add hookdeck/webhook-skills --skill daytona-webhooks
Daytona runs sandboxes for AI-generated code. It sends webhooks when sandboxes, snapshots and volumes are created, change state, or are removed.
Where the signature scheme comes from: Daytona's public webhooks page documents the events and payloads but says nothing about signing or verification. The scheme below is Svix's: Daytona's source shows deliveries go out through Svix, and the dashboard reads the Svix endpoint secret. Daytona does not document it, so confirm it against your own deliveries (see references/verification.md).
When to Use This Skill
- How do I receive Daytona webhooks?
- How do I verify a Daytona webhook signature (
svix-signature)? - How do I react when a sandbox changes state (
sandbox.state.updated)? - How do I handle
snapshot.createdorvolume.createdevents? - Why is my Daytona webhook signature verification failing?
- Why am I processing the same Daytona event twice?
Verification (core)
| Header | Value |
|---|---|
svix-id | Message ID. Same on every retry of the same message |
svix-timestamp | Unix seconds when the attempt was sent |
svix-signature | Space-delimited list of v1,<base64> signatures |
Signed content is {svix-id}.{svix-timestamp}.{raw body}, HMAC-SHA256, base64. The key is the base64-decoded part of the secret after whsec_. Compare in constant time against every v1, entry.
import crypto from 'node:crypto';
// rawBody: the exact bytes received (express.raw() / request.text()), NOT re-serialised JSON.
// toleranceSec = 0 (default) skips the timestamp window, because Hookdeck retries and replays
// keep the original svix-timestamp. A positive value opts in.
function verifyDaytonaWebhook(rawBody, headers, secret, toleranceSec = 0) {
const id = headers['svix-id'];
const ts = headers['svix-timestamp'];
const sigs = headers['svix-signature'];
if (!id || !/^\d+$/.test(ts ?? '') || !sigs) throw new Error('Missing or malformed svix-* headers');
if (toleranceSec > 0 && Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) {
throw new Error('Timestamp outside tolerance');
}
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); // decode; don't use the string as-is
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.`).update(rawBody).digest();
const ok = sigs.split(' ').some((entry) => {
const [version, sig] = entry.split(',');
const given = Buffer.from(sig ?? '', 'base64');
return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
if (!ok) throw new Error('Invalid signature');
return JSON.parse(rawBody.toString('utf8'));
}
Why not the svix SDK? new Webhook(secret).verify(...) (npm) and Webhook(secret).verify(...) (PyPI) check the same signature, but they always enforce a 5-minute timestamp window with no way to turn it off. Hookdeck retries and replays carry the original svix-timestamp, so the SDK rejects them. The examples verify manually and make the window opt-in with DAYTONA_ (default 0; a positive number of seconds turns it on; anything that isn't a whole number is treated as 0, with a warning at startup). If you never retry or replay late, the SDK is fine.
The Python version is in references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload Shape and Deduplication
Payloads are flat JSON. The discriminator is event, with an ISO 8601 timestamp:
{ "event": "sandbox.state.updated", "timestamp": "2025-12-19T10:30:00.000Z",
"id": "sandbox123", "organizationId": "org123",
"oldState": "started", "newState": "stopped", "updatedAt": "2025-12-19T10:30:00.000Z" }
The payload has no event ID, so deduplicate on svix-id, which stays the same when Svix retries a message.
Common Event Types
| Event | Fired when | Key fields |
|---|---|---|
sandbox.created | A new sandbox is created | id, organizationId, state, sandboxClass, createdAt |
sandbox.state.updated | A sandbox's state changes | id, organizationId, oldState, newState, updatedAt |
snapshot.created | A new snapshot is created | id, name, organizationId, state, createdAt |
snapshot.state.updated | A snapshot's state changes | id, name, organizationId, oldState, newState, updatedAt |
snapshot.removed | A snapshot is removed | id, name, organizationId, removedAt |
volume.created | A new volume is created | id, name, organizationId, state, createdAt |
volume.state.updated | A volume's state changes | id, name, organizationId, oldState, newState, updatedAt |
Environment Variables
DAYTONA_WEBHOOK_SECRET=whsec_your_endpoint_secret # Svix endpoint signing secret
DAYTONA_TIMESTAMP_TOLERANCE_SECONDS=0 # Optional replay window; a positive value opts in
Local Development
npx hookdeck-cli listen 3000 daytona --path /webhooks/daytona
No account required. The CLI creates a guest account on first run and gives you a tunnel and a web UI for inspecting requests. Use port 8000 for the FastAPI example.
Reference Materials
- references/overview.md - What Daytona webhooks are, events and payloads
- references/setup.md - Create an endpoint in the Daytona Dashboard and get the secret
- references/verification.md - Svix verification details, manual fallback, gotchas