Overview
Receive signed, automatically-retried HTTP callbacks when events happen in your organization.
Webhooks
Webhooks let you react to events — a cluster coming online, a VM stopping —
without polling. OpenRelay POSTs a JSON payload to a URL you control, signs
each delivery, and retries on failure.
Create a webhook
Register an endpoint and the events you care about. The response includes the
signing secret (whsec_…), shown once:
curl -X POST https://api.openrelay.inc/v1/orgs/$ORG_ID/webhooks/create \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "deploys",
"url": "https://example.com/hooks/openrelay",
"events": ["cluster.running", "cluster.failed"]
}'{
"id": "wh_…",
"url": "https://example.com/hooks/openrelay",
"events": ["cluster.running", "cluster.failed"],
"enabled": true,
"secret": "whsec_…", // store this now — shown once
"secretPrefix": "whsec_abcd"
}Lost the secret?
You can't read a secret again, but you can roll it with
/webhooks/{id}/regenerate-secret.
An empty events array subscribes to all events.
Event types
| Event | Fires when |
|---|---|
cluster.running | A cluster reaches a healthy, serving state. |
cluster.degraded | Some replicas are unhealthy. |
cluster.failed | A cluster fails. |
replica.healthy | A replica becomes healthy. |
replica.unhealthy | A replica becomes unhealthy. |
replica.terminated | A replica is terminated. |
vm.running | A VM reaches the running state. |
vm.stopped | A VM is stopped. |
vm.terminated | A VM is terminated. |
Payload
Every delivery is a JSON body of this shape:
{
"id": "evt_…",
"type": "cluster.running",
"created_at": "2026-06-10T12:00:00Z",
"data": { "clusterId": "cl_…" }
}Each request also carries these headers:
| Header | Description |
|---|---|
X-VectorLay-Signature | sha256=<hex> HMAC of the raw body (see below). |
X-VectorLay-Event | The event type, e.g. cluster.running. |
X-VectorLay-Delivery | Unique id for this delivery attempt. |
Verify the signature
Always verify
Treat unsigned or badly-signed requests as forgeries. Verify the signature on the raw request body before parsing it.
The signature is computed as:
X-VectorLay-Signature = "sha256=" + hex( HMAC_SHA256(key, rawBody) )where the HMAC key is the lowercase hex SHA-256 of your whsec_… secret —
i.e. hex(sha256(secret)). Compute the key once and reuse it.
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
// Derive the signing key from your stored secret (do this once).
function signingKey(secret: string) {
return createHash('sha256').update(secret).digest('hex');
}
export function verify(rawBody: string, header: string, secret: string) {
const expected =
'sha256=' +
createHmac('sha256', signingKey(secret)).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && timingSafeEqual(a, b);
}import hashlib, hmac
def signing_key(secret: str) -> bytes:
return hashlib.sha256(secret.encode()).hexdigest().encode()
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(signing_key(secret), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)Use the raw bytes
Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization — reformatting the body changes the signature.
Responding, retries & delivery
- Respond
2xxquickly. Acknowledge fast and do slow work asynchronously; receivers that take too long are treated as failures. - Retries. Failed deliveries are retried up to 3 times with backoff
(~10s, 60s, 300s). Use
X-VectorLay-Deliveryto deduplicate — the same event may arrive more than once. - Inspect deliveries with
/webhooks/{id}/deliveriesto see recent attempts and response codes. - Pause a webhook with
/webhooks/{id}/toggleinstead of deleting it.