Webhooks API

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_countintegerNoConsecutive failed deliveries before the webhook is marked failed (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.

The attempt is recorded in the delivery log like any other delivery, marked as a test, so what the endpoint received is always visible at GET /api/v1/webhooks/{id}/deliveries. A test does not count toward retry_count and never moves the webhook to failed: a test against an endpoint that is temporarily down must not pause a live subscription.

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 on a fixed backoff schedule:

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.

A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within 10 seconds. retry_count (0-10, default 3) is the number of consecutive failed deliveries after which MIOSA marks the webhook failed; it is not the per-delivery retry cap. Set it to 0 to mark the webhook failed on the first failed delivery.


Subscribed Event Types

GET /api/v1/webhooks/events returns this list, grouped by product surface. It is the same list the API validates a subscription against, so an event that is not here cannot be subscribed to.

EventDescription
sandbox.createdSandbox created
sandbox.startingSandbox boot started
sandbox.readySandbox became usable: ready turned true, whether that was decided on boot or by a readiness probe
sandbox.startedSandbox runtime is up and reachable
sandbox.pausedSandbox left running for paused
sandbox.resumedA paused sandbox came back up
sandbox.idle_pausedSandbox was paused because it sat idle past its timeout
sandbox.errorSandbox failed to start or crashed
sandbox.destroyedSandbox deleted
computer.createdComputer created
computer.startedComputer finished booting and is usable
computer.stoppedComputer was asked to stop
computer.deletedComputer destroyed
database.createdDatabase provisioned
database.readyDatabase accepting connections
database.stoppedDatabase stopped
database.deletedDatabase deleted
managed_database.provisioningManaged database provisioning started
managed_database.runningManaged database running
managed_database.failedManaged database provisioning failed
managed_database.destroyedManaged database destroyed
managed_service.provisioningManaged service provisioning started
managed_service.runningManaged service running
managed_service.failedManaged service provisioning failed
managed_service.destroyedManaged service destroyed
deployment.createdDeployment created
deployment.buildingBuild started
deployment.build_startedBuild step began executing
deployment.build_succeededBuild produced an artifact
deployment.build_failedBuild failed
deployment.readyDeployment serving traffic
deployment.errorDeployment failed or crashed
deployment.canceledDeployment canceled
deployment.rolled_backDeployment rolled back to a previous version
preview.createdPull-request preview created
preview.activePreview serving traffic
preview.destroyedPreview torn down
health_check.healthyEndpoint health check passed
health_check.unhealthyEndpoint health check failed
cron.execution_startedScheduled job started
cron.execution_completedScheduled job finished
cron.execution_failedScheduled job failed
billing.credits_lowCredit balance below the configured threshold
billing.credits_exhaustedCredit balance reached zero
billing.sandbox.spend_threshold_reachedSandbox spend crossed its threshold
run.createdAgent run created
run.queuedAgent run queued
run.runningAgent run started
run.progressAgent run reported progress
run.succeededAgent run succeeded
run.failedAgent run failed
run.canceledAgent run canceled
run.file_readyA file produced by an agent run is ready
forge.pushA branch in a Forge repository received commits
forge.pull_requestA Forge pull request was opened, reopened, closed or merged
forge.checkA check reported a result on a Forge commit

forge.* payloads carry repository_id, repository_slug and organization_slug, so one subscription can route each event to the right repository.

webhook.test is the synthetic event sent by POST /api/v1/webhooks/:id/test. It is not subscribable and appears in the delivery log as a test delivery.

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


Examples

Was this page helpful?