Guides
Webhooks
Receive signed HTTPS events when machine lifecycle operations finish, verify their signatures, and handle retries.
Webhooks tell your service when machines become ready, stop, start, are deleted, or fail. VMYard sends a signed JSON POST to each HTTPS endpoint you register, and retries until the endpoint answers with a 2xx status.
Events
Events come from the final, observed result of an operation, never from anything a machine reports about itself.
| Event | Sent when |
|---|---|
machine.ready | A create succeeded: the machine was observed running. This includes a live fork and a woken restore. |
machine.started | A start or restart succeeded. |
machine.stopped | A stop succeeded, including auto-stops, idle sleep, and stops because the balance ran out, or a fork or restore created its machine stopped. |
machine.deleted | A delete succeeded: the machine was confirmed gone. |
machine.error | A create, start, stop, restart, or delete ended failed or correlation_miss. |
operation.failed | Any public operation (lifecycle, command, background command, kill, file write, port or SSH change, internet access change, environment install, snapshot push, template save) ended failed or correlation_miss. |
snapshot.ready | A snapshot push succeeded. data.snapshot is {id, template_name, tier, status}. |
template.ready | A template save succeeded. data.snapshot names the template. |
An uncertain operation sends nothing until it settles. Successful command, file, port, SSH, and internet access operations send nothing. A restart never stops its machine, so it sends machine.started and never machine.stopped.
{
"id": "6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"type": "machine.ready",
"created_at": "2026-10-12T05:51:01.738927Z",
"organization_id": "2a7d4c1e-8b3f-4f6a-9c5d-0e1f2a3b4c5d",
"data": {
"machine": { "id": "0b6f6c55-2f1d-4f0e-9a51-6f0f4cba7a10", "name": "dev", "network": { "egress": "internet", "ipv6_address": null } },
"operation": { "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "kind": "create", "status": "succeeded", "error_code": null },
"state": { "desired": "running", "observed": "running" }
}
}data.machine.name is the machine's display name, and data.machine.network its egress policy and IPv6 address as its host last reported them, or null before any report. Events of a stop operation also carry data.operation.stop_reason: api (a caller asked for it), auto_stop, billing, or idle_sleep. Other events don't have the field.
Register an endpoint
Only Owners and Admins can see or change webhooks, because endpoint URLs can contain secrets. API keys need the full_access preset, and their creator must still be an Owner or Admin. You can also manage endpoints in the console under Webhooks.
vy webhooks add --name ci --url https://hooks.example.com/vmyard --event machine.ready --event machine.error
vy webhooks lsThe answer includes the signing secret (whsec_…). It is shown only now and when you rotate it; store it with your receiver.
name: 1–64 characters.url:https, port 443, at most 2048 bytes, no username, password, or fragment, and a public host. Private, loopback, link-local, and other non-public addresses and names such aslocalhostor*.internalare refused.events: one or more of the events above, without duplicates. The CLI subscribes to all eight when you give no--event.enabled:trueby default.
An organization can have up to 10 endpoints (409 webhook_limit_reached).
Verify signatures
Every delivery carries these headers:
| Header | Value |
|---|---|
X-VMYard-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-VMYard-Event-Id | The event id. The same for every endpoint and every attempt. |
X-VMYard-Event-Type | The event type. |
X-VMYard-Delivery-Attempt | 1 for the first attempt, then 2, 3, … |
User-Agent | VMYard-Webhooks/1 |
v1 is the HMAC-SHA256 of t + "." + body, keyed with the whole secret string including whsec_. Each attempt is signed again with a fresh timestamp. To verify:
- Read the raw request body before parsing it.
- Recompute the HMAC and compare it in constant time.
- Reject timestamps more than 5 minutes old.
- Skip any
X-VMYard-Event-Idyou have already processed.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body as received (Buffer), before JSON parsing.
export function verifyVmyard(secret, rawBody, header, tolerance = 300) {
const fields = Object.fromEntries(
header.split(",").map((part) => {
const at = part.indexOf("=");
return [part.slice(0, at), part.slice(at + 1)];
}),
);
const timestamp = Number(fields.t);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
const expected = createHmac("sha256", secret)
.update(`${fields.t}.`)
.update(rawBody)
.digest();
const received = Buffer.from(fields.v1 ?? "", "hex");
return (
received.length === expected.length &&
timingSafeEqual(received, expected)
);
}Retries and ordering
A 2xx answer delivers the event. Anything else is a failed attempt: other statuses, redirects (never followed), no answer within 10 seconds, a connection failure, or a destination that resolves to a non-public address. VMYard retries after 1, 2, 4, … minutes, up to 6 hours apart; after 12 attempts (about 20.5 hours) the delivery is marked dead.
Delivery is at least once and unordered: the same event can arrive twice, and events can arrive out of order. Deduplicate on X-VMYard-Event-Id, and order by created_at or by reading the machine's current state rather than by arrival.
Deliveries
vy webhooks deliveries ci --limit 20Deliveries are listed newest first with status (pending, delivering, succeeded, or dead), attempt_count, next_attempt_at, last_status_code, and last_error.
Pause, change, rotate, delete
- Disable an endpoint (
PATCHwith{"enabled": false}) to pause it. Queued deliveries wait until you enable it again; no new events are queued while it is disabled. - Change its
name,url, oreventswithPATCH; omitted fields keep their value. - Rotate the secret (
POST /v1/webhooks/{id}/rotate-secret, orvy webhooks rotate). The old secret stops signing at once, including retries of queued events, so update your receiver promptly. - Delete the endpoint (
DELETE, orvy webhooks rm). Its queued and past deliveries are deleted with it.