Skip to content

Guides

Webhooks

Receive signed HTTPS events when machine lifecycle operations finish, verify their signatures, and handle retries.

On this page

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.

EventSent when
machine.readyA create succeeded: the machine was observed running. This includes a live fork and a woken restore.
machine.startedA start or restart succeeded.
machine.stoppedA stop succeeded, including auto-stops, idle sleep, and stops because the balance ran out, or a fork or restore created its machine stopped.
machine.deletedA delete succeeded: the machine was confirmed gone.
machine.errorA create, start, stop, restart, or delete ended failed or correlation_miss.
operation.failedAny 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.readyA snapshot push succeeded. data.snapshot is {id, template_name, tier, status}.
template.readyA 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.

machine.ready
{
  "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 ls

The 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 as localhost or *.internal are refused.
  • events: one or more of the events above, without duplicates. The CLI subscribes to all eight when you give no --event.
  • enabled: true by default.

An organization can have up to 10 endpoints (409 webhook_limit_reached).

Verify signatures

Every delivery carries these headers:

HeaderValue
X-VMYard-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-VMYard-Event-IdThe event id. The same for every endpoint and every attempt.
X-VMYard-Event-TypeThe event type.
X-VMYard-Delivery-Attempt1 for the first attempt, then 2, 3, …
User-AgentVMYard-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:

  1. Read the raw request body before parsing it.
  2. Recompute the HMAC and compare it in constant time.
  3. Reject timestamps more than 5 minutes old.
  4. Skip any X-VMYard-Event-Id you 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 20

Deliveries 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 (PATCH with {"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, or events with PATCH; omitted fields keep their value.
  • Rotate the secret (POST /v1/webhooks/{id}/rotate-secret, or vy webhooks rotate). The old secret stops signing at once, including retries of queued events, so update your receiver promptly.
  • Delete the endpoint (DELETE, or vy webhooks rm). Its queued and past deliveries are deleted with it.