Guide to Daytona Webhooks: Features and Best Practices
Daytona provides sandboxes for running AI-generated code, along with the snapshots and volumes behind them. Its webhooks tell you when a sandbox is created or changes state, when a snapshot finishes building, and when a volume is ready, so you can react without polling the API.
This guide covers how Daytona webhook endpoints work, the seven events and their payloads, how deliveries are signed and verified, and what to plan for given how little delivery behaviour Daytona documents.
What are Daytona webhooks?
Daytona sends a JSON POST to each endpoint in your organisation that subscribes to an event. Endpoints are managed in the Daytona Dashboard under Webhooks, which is available to every member of the organisation. If you don't see it, Daytona asks you to contact support@daytona.io to enable it.
Daytona's webhooks page documents events and payloads but says nothing about signatures. Daytona's open-source webhook service shows that deliveries go out through Svix, so each request carries Svix's svix-id, svix-timestamp and svix-signature headers, signed with a whsec_ endpoint secret. Because this comes from Daytona's code rather than its docs, check the headers on your first real delivery.
Daytona webhook features
| Feature | Details |
|---|---|
| Configuration | Daytona Dashboard, Webhooks > Add Endpoint |
| Endpoint settings | Name, HTTPS URL and subscribed events |
| Event field | event in the body |
| Signature headers | svix-id, svix-timestamp, svix-signature (from Daytona's source, not its docs) |
| Scheme | v1,<base64 HMAC-SHA256> over {svix-id}.{svix-timestamp}.{body} |
| Secret | whsec_ endpoint secret, base64-decoded after the prefix to form the key |
| Delivery logs | Messages tab, with each attempt's status code, duration and response body |
| Timeout, retries, ordering | Not documented by Daytona |
| Source IPs | Not documented |
Common events
| Event | Fires when |
|---|---|
sandbox.created | A sandbox is created |
sandbox.state.updated | A sandbox changes state, for example from started to stopped |
snapshot.created | A snapshot is created |
snapshot.state.updated | A snapshot changes state, for example from building to active |
snapshot.removed | A snapshot is removed |
volume.created | A volume is created |
volume.state.updated | A volume changes state, for example from creating to ready |
Payloads are flat. The two common fields, event and an ISO 8601 timestamp, sit next to the resource's own fields. Here is Daytona's sandbox.state.updated example:
{
"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"
}
Created events carry state and createdAt. State events carry oldState, newState and updatedAt. snapshot.removed carries removedAt. Snapshot and volume events also include name, and sandbox.created adds sandboxClass.
The id field is the resource's ID, not an event ID. There is no event ID in the body, so use the svix-id header to deduplicate. The state values in the public docs are lower case, but Daytona's service README shows upper-case examples such as STARTED, so compare states case-insensitively.
Setting up Daytona webhooks
- In the Daytona Dashboard, click Webhooks in the sidebar, then Add Endpoint.
- Enter a name, your HTTPS URL and the events you want, then click Create.
- Open the endpoint's details page and copy its signing secret, which starts with
whsec_. Daytona's docs don't describe the secret, so ask Daytona support if you can't find it. - Trigger an event, for example by creating a sandbox, and check the Messages tab. Each message lists its delivery attempts with the status code and response body your endpoint returned.
Securing Daytona webhooks
Verification follows the Svix scheme:
- The signed content is
{svix-id}.{svix-timestamp}.{raw body}. - The key is the base64-decoded part of the secret after
whsec_. svix-signatureis a space-delimited list ofv1,<base64 signature>entries. Accept the request if any of them match.
Svix's SDKs always enforce a five-minute timestamp window, which fails deliveries that a queue or replay tool sends on later. The manual version below makes that check optional and off by default:
const crypto = require('crypto');
const express = require('express');
// 0 skips the svix-timestamp check; set a positive number of seconds (for example 300) to enable it.
const TIMESTAMP_TOLERANCE_SECONDS = Number(process.env.DAYTONA_TIMESTAMP_TOLERANCE_SECONDS || 0);
function verifyDaytonaSignature(rawBody, headers, secret) {
const msg_id = headers['svix-id'];
const timestamp = headers['svix-timestamp'];
const signature_header = headers['svix-signature'];
if (!msg_id || !timestamp || !signature_header || !secret) return false;
if (TIMESTAMP_TOLERANCE_SECONDS > 0) {
const now = Math.floor(Date.now() / 1000);
const sent_at = Number(timestamp);
if (!Number.isFinite(sent_at) || Math.abs(now - sent_at) > TIMESTAMP_TOLERANCE_SECONDS) return false;
}
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const signed_content = Buffer.concat([Buffer.from(`${msg_id}.${timestamp}.`, 'utf8'), rawBody]);
const expected = crypto.createHmac('sha256', key).update(signed_content).digest('base64');
const computed = Buffer.from(expected, 'utf8');
return signature_header.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
});
}
const app = express();
app.post('/webhooks/daytona', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.DAYTONA_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyDaytonaSignature(req.body, req.headers, secret)) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// Deduplicate on req.get('svix-id'), route on event.event, then process async.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
import time
TIMESTAMP_TOLERANCE_SECONDS = int(os.environ.get("DAYTONA_TIMESTAMP_TOLERANCE_SECONDS", "0"))
def verify_daytona_signature(raw_body: bytes, headers, secret: str) -> bool:
msg_id = headers.get("svix-id")
timestamp = headers.get("svix-timestamp")
signature_header = headers.get("svix-signature")
if not (msg_id and timestamp and signature_header and secret):
return False
if TIMESTAMP_TOLERANCE_SECONDS > 0:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TIMESTAMP_TOLERANCE_SECONDS:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed_content = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
return True
return False
Make Daytona webhooks production-ready. Hookdeck Event Gateway verifies Daytona's Svix signature, records every delivery, and lets you retry and replay sandbox, snapshot and volume events on your own schedule.
In Hookdeck, create a Daytona source and paste the endpoint's whsec_ secret. Hookdeck checks the svix-* headers and also accepts the webhook-* names that Standard Webhooks uses. Verification is optional on the source, so you can leave it off while you confirm the headers on your first delivery.
Daytona webhook limitations and pain points
Signing isn't in Daytona's docs
The Problem: You have to work out how to verify deliveries from somewhere other than Daytona's documentation.
Why It Happens: Daytona's webhooks page covers events and payloads only. The Svix scheme comes from Daytona's open-source code and Svix's own documentation.
Workarounds:
- Log the headers of your first delivery and confirm the
svix-*names before you enforce verification. - Keep the secret per endpoint and rotate it if it leaks.
How Hookdeck Can Help: Hookdeck shows the full headers and body of each request, so you can confirm the scheme from a real delivery, then turn verification on for the source.
No documented retry or timeout behaviour
The Problem: You can't tell how long Daytona will keep retrying a failed delivery, or how fast you need to respond.
Why It Happens: Daytona documents no timeout, retry schedule, ordering or disable policy. The Messages tab shows delivery attempts, but not the rules behind them.
Workarounds:
- Return
2xxquickly and process asynchronously. - Treat the Daytona API as the source of truth and reconcile sandbox state after an outage.
How Hookdeck Can Help: Hookdeck acknowledges Daytona's request at once, stores it, and applies a retry policy you control when delivering to your service.
No event ID in the payload
The Problem: The body's id identifies the sandbox, snapshot or volume, so it repeats across many events.
Why It Happens: Daytona's payloads are flat resource summaries without an event identifier.
Workarounds:
- Deduplicate on
svix-id, which stays the same when a message is retried. - For state events, apply a change only if
updatedAtis newer than what you have stored.
How Hookdeck Can Help: Hookdeck can deduplicate on a header such as svix-id before events reach your service.
Best practices
Deduplicate on svix-id
Store it before processing and return 200 for repeats. See our guide to webhook idempotency.
Order state changes by updatedAt
Daytona doesn't promise ordering. Track the latest updatedAt per resource and ignore older sandbox.state.updated events.
Subscribe to what you need
sandbox.state.updated can be frequent if you start and stop many sandboxes. Subscribe each endpoint only to the events its consumer handles.
Handle unknown event types
Daytona's API can send custom-typed messages to every endpoint in an organisation, so log and acknowledge events you don't recognise instead of returning an error.
Conclusion
Daytona webhooks cover seven sandbox, snapshot and volume events with small, flat payloads. Signing isn't documented by Daytona, but deliveries go out through Svix, so verify svix-signature with the endpoint's whsec_ secret, deduplicate on svix-id, and compare states case-insensitively.
Hookdeck Event Gateway verifies the signature and gives you the retry and replay policy that Daytona doesn't document.
Get started with Hookdeck for free and handle Daytona webhooks reliably in minutes.