On this page

Webhooks fire outgoing HTTP POST requests to your endpoint when platform events occur - sandbox lifecycle changes, deployment completions, billing events, and more. Each delivery is signed with HMAC-SHA256 so you can verify authenticity.

Base path: /api/v1/webhooks


Endpoints

MethodPathDescription
GET/api/v1/webhooksList all webhooks
POST/api/v1/webhooksCreate a webhook
GET/api/v1/webhooks/{id}Get a webhook
PATCH/api/v1/webhooks/{id}Update a webhook
DELETE/api/v1/webhooks/{id}Delete a webhook
POST/api/v1/webhooks/{id}/testSend a test delivery
GET/api/v1/webhooks/{id}/deliveriesList delivery attempts

List Webhooks

GET /api/v1/webhooks

Auth

Authorization: Bearer msk_...

Response - 200 OK

{
  "data": [
    {
      "id": "wh_01hwxyz...",
      "name": "My Webhook",
      "url": "https://api.myapp.com/hooks/miosa",
      "events": ["sandbox.created", "sandbox.destroyed"],
      "secret_preview": "sk_live_••••••••abcd",
      "retry_count": 3,
      "created_at": "2026-05-01T00:00:00Z",
      "updated_at": "2026-05-01T00:00:00Z"
    }
  ]
}

Create a Webhook

POST /api/v1/webhooks

Request Body

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint to POST events to
namestringNoHuman-readable label
eventsstring[]NoEvent type filter; empty = all events
headersobjectNoExtra headers to include in deliveries
retry_countintegerNoNumber of retries on failure (0-10, default 3)
metadataobjectNoArbitrary key-value metadata
{
  "url": "https://api.myapp.com/hooks/miosa",
  "name": "Sandbox lifecycle",
  "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"],
  "retry_count": 3
}

Response - 201 Created

{
  "data": {
    "id": "wh_01hwxyz...",
    "name": "Sandbox lifecycle",
    "url": "https://api.myapp.com/hooks/miosa",
    "events": ["sandbox.created", "sandbox.ready", "sandbox.error", "sandbox.destroyed"],
    "secret": "sk_live_abc123def456...",
    "secret_preview": "sk_live_••••••••3456",
    "retry_count": 3,
    "created_at": "2026-05-26T10:14:23Z",
    "updated_at": "2026-05-26T10:14:23Z"
  }
}

The secret field appears only in this response. Save it securely.


Update a Webhook

PATCH /api/v1/webhooks/{id}

Partial update - include only fields you want to change.

Request Body

Same fields as create, all optional.

Response - 200 OK

Webhook object without the secret field.


Delete a Webhook

DELETE /api/v1/webhooks/{id}

Response - 204 No Content


Send a Test Delivery

POST /api/v1/webhooks/{id}/test

Sends a synthetic webhook.test event to the configured URL to verify connectivity and signature verification.

Response - 200 OK

{
  "delivered": true,
  "status_code": 200,
  "duration_ms": 143
}

List Deliveries

GET /api/v1/webhooks/{id}/deliveries

Returns recent delivery attempts with status codes and timing.

Response - 200 OK

{
  "data": [
    {
      "id": "del_01hwyza...",
      "event_type": "sandbox.ready",
      "status": "delivered",
      "status_code": 200,
      "attempt": 1,
      "duration_ms": 98,
      "delivered_at": "2026-05-26T10:14:24Z"
    },
    {
      "id": "del_01hwyzb...",
      "event_type": "sandbox.created",
      "status": "failed",
      "status_code": 500,
      "attempt": 3,
      "duration_ms": 5012,
      "delivered_at": "2026-05-26T10:14:20Z"
    }
  ]
}

Signature Verification

Every delivery includes an X-Miosa-Signature header. Verify it on your server before trusting the payload:

X-Miosa-Signature: sha256=<hex_digest>

Compute the expected signature:

hmac = HMAC-SHA256(secret, raw_request_body_bytes)
expected = "sha256=" + hex(hmac)
assert constant_time_equals(expected, request.headers["X-Miosa-Signature"])

Example verification (Python)

import hmac, hashlib

def verify_signature(secret: str, body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)

Example verification (TypeScript)

import { createHmac, timingSafeEqual } from 'crypto';

function verifySignature(secret: string, body: Buffer, header: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

Retry Policy

Failed deliveries are retried with exponential backoff:

AttemptDelay
230 seconds
35 minutes
4+Up to retry_count × 10 minutes (capped at 6 hours)

A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within 30 seconds. Set retry_count: 0 to disable retries.


Subscribed Event Types

EventDescription
sandbox.createdSandbox created
sandbox.readySandbox reached running
sandbox.errorSandbox failed to start
sandbox.destroyedSandbox deleted
computer.createdComputer created
computer.runningComputer reached running
computer.stoppedComputer stopped
computer.deletedComputer deleted
deployment.succeededBuild succeeded and promoted
deployment.failedBuild or promotion failed
deployment.rollbackRollback triggered
billing.credit_lowCredit balance below threshold
billing.credit_exhaustedCredit balance reached zero
webhook.testSynthetic test delivery

Subscribe to all events by omitting the events field. Subscribe to a subset by listing specific event types.


Examples

Was this helpful?