Webhooks
MIOSA sends HTTP POST requests to your endpoint whenever a sandbox or deployment changes state. Use webhooks to update your database, notify users, trigger downstream actions, or bill for compute.
Event taxonomy
Sandbox events
| Event | Fires when |
|---|---|
sandbox.created | The sandbox row is inserted. The VM may still be booting. |
sandbox.running | The sandbox is fully booted and ready. external_user_id is available. |
sandbox.paused | The sandbox was paused due to an idle timeout or an explicit API call. |
sandbox.resumed | A paused sandbox was resumed. |
sandbox.destroyed | The sandbox was permanently deleted. |
sandbox.error | The sandbox failed to boot or encountered a runtime error. |
Deployment events
| Event | Fires when |
|---|---|
deployment.created | A new deployment was created. |
deployment.build_started | A build started for a new version. |
deployment.build_succeeded | A build finished successfully. |
deployment.build_failed | A build failed. The error field contains the failure reason. |
deployment.published | A version was promoted to the active release. |
deployment.domain_attached | A custom domain was verified and attached. |
Payload structure
Every webhook delivery has the same envelope:
{
"id": "evt_01hzqmrkntq6g9gxnqhvpa8c7t",
"type": "sandbox.running",
"created_at": "2026-05-25T14:32:10Z",
"data": {
"id": "sbx_01hzq5pnmpgt6vdwp0r8d23c5n",
"state": "running",
"external_user_id": "user_42",
"external_project_id": "proj_opendesigns_1",
"metadata": {
"customer_id": "user_42",
"project_name": "brand-refresh",
"source": "opendesigns-whitelabel"
},
"preview_url": "https://3000-sbx01hzq.sandbox.opendesigns.ai"
}
} The id field is a unique event ID. Use it to deduplicate deliveries.
HMAC-SHA256 signature verification
Every delivery includes a Miosa-Signature header:
Miosa-Signature: t=1716645130,v1=a3f2c8... t- Unix timestamp (seconds) when MIOSA signed the requestv1- HMAC-SHA256 hex digest of"<t>.<raw_body>"
The signed payload is timestamp + "." + raw request body bytes. Verify before parsing the JSON.
Idempotency
MIOSA may deliver the same event more than once (network retries, at-least-once semantics). Make your handler idempotent by storing the event id and discarding duplicates:
processed_events = set() # use a database or cache in production
def handle_event(event: dict) -> None:
event_id = event["id"]
if event_id in processed_events:
return # duplicate - skip
processed_events.add(event_id)
match event["type"]:
case "sandbox.running":
notify_user(event["data"]["external_user_id"])
case "sandbox.destroyed":
release_resources(event["data"]["id"])
case "sandbox.error":
alert_oncall(event["data"]) Retry policy
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | 5 seconds |
| 3 | 30 seconds |
| 4 | 5 minutes |
| 5 | 30 minutes |
MIOSA stops retrying after five attempts. If your endpoint is unreachable for more than 30 minutes, check the webhook delivery log in the MIOSA dashboard.
Return a 2xx status code within 10 seconds to acknowledge receipt. If your processing takes longer, acknowledge immediately and handle the event asynchronously.
Register a webhook
Local testing
Use a tunnel to expose your local server during development:
# Option 1 - ngrok
ngrok http 8000
# Copy the https URL, set MIOSA_WEBHOOK_URL in .env
# Option 2 - cloudflared
cloudflared tunnel --url http://localhost:8000 Then run the one-time setup script to register the tunnel URL as your webhook endpoint. When you restart the tunnel and get a new URL, update the webhook:
# Update an existing webhook's URL
client.webhooks.update("whk_abc123", url="https://new-tunnel.ngrok.io/webhooks/miosa")