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

EventFires when
sandbox.createdThe sandbox row is inserted. The VM may still be booting.
sandbox.runningThe sandbox is fully booted and ready. external_user_id is available.
sandbox.pausedThe sandbox was paused due to an idle timeout or an explicit API call.
sandbox.resumedA paused sandbox was resumed.
sandbox.destroyedThe sandbox was permanently deleted.
sandbox.errorThe sandbox failed to boot or encountered a runtime error.

Deployment events

EventFires when
deployment.createdA new deployment was created.
deployment.build_startedA build started for a new version.
deployment.build_succeededA build finished successfully.
deployment.build_failedA build failed. The error field contains the failure reason.
deployment.publishedA version was promoted to the active release.
deployment.domain_attachedA custom domain was verified and attached.

Forge events

Every Forge event names its repository in the payload (repository.id, repository.full_name, repository.owner, repository.slug), so one organization subscription can route events from every repository. Subscriptions cannot be limited to a single repository.

EventFires when
forge.pushA push to a repository was accepted.
forge.pull_requestA pull request was opened, closed, reopened, merged or reviewed.
forge.checkA check reported on a pull request or a commit. The payload includes commit_sha.

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_lumen_1",
    "metadata": {
      "customer_id": "user_42",
      "project_name": "brand-refresh",
      "source": "lumen-whitelabel"
    },
    "preview_url": "https://3000-sbx01hzq.sandbox.example.com"
  }
}

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 request
  • v1 - 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

AttemptDelay
1 (initial)Immediate
21 minute
35 minutes
430 minutes
51 hour
62 hours
74 hours
88 hours

MIOSA makes up to eight delivery attempts over roughly 15.5 hours (within a 24-hour window), then drops the event. If your endpoint is unreachable, 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")

See also

Was this page helpful?