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.created or volume.created events?
  • Why is my Daytona webhook signature verification failing?
  • Why am I processing the same Daytona event twice?

Verification (core)

HeaderValue
svix-idMessage ID. Same on every retry of the same message
svix-timestampUnix seconds when the attempt was sent
svix-signatureSpace-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_TIMESTAMP_TOLERANCE_SECONDS (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

EventFired whenKey fields
sandbox.createdA new sandbox is createdid, organizationId, state, sandboxClass, createdAt
sandbox.state.updatedA sandbox's state changesid, organizationId, oldState, newState, updatedAt
snapshot.createdA new snapshot is createdid, name, organizationId, state, createdAt
snapshot.state.updatedA snapshot's state changesid, name, organizationId, oldState, newState, updatedAt
snapshot.removedA snapshot is removedid, name, organizationId, removedAt
volume.createdA new volume is createdid, name, organizationId, state, createdAt
volume.state.updatedA volume's state changesid, 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →