Webhooks

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

EventFires when
cluster.runningA cluster reaches a healthy, serving state.
cluster.degradedSome replicas are unhealthy.
cluster.failedA cluster fails.
replica.healthyA replica becomes healthy.
replica.unhealthyA replica becomes unhealthy.
replica.terminatedA replica is terminated.
vm.runningA VM reaches the running state.
vm.stoppedA VM is stopped.
vm.terminatedA 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:

HeaderDescription
X-VectorLay-Signaturesha256=<hex> HMAC of the raw body (see below).
X-VectorLay-EventThe event type, e.g. cluster.running.
X-VectorLay-DeliveryUnique 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.

Node.js / TypeScript
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);
}
Python
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 2xx quickly. 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-Delivery to deduplicate — the same event may arrive more than once.
  • Inspect deliveries with /webhooks/{id}/deliveries to see recent attempts and response codes.
  • Pause a webhook with /webhooks/{id}/toggle instead of deleting it.

On this page