Authentication

Every signed request carries an HMAC-SHA256 signature of the body computed with your team's webhook secret.

The secret

Each team has a single webhook secret — a 64-character hex string generated when the team is created. Find it under Settings → Webhook Secret. It is shown in full once and stored hashed thereafter, so copy it into your secret manager immediately.

Signing a request

The signature is HMAC-SHA256(body, secret), hex-encoded, sent in the X-Webhook-Signature header. The body is the raw request bytes — sign first, send the same bytes verbatim.

async function sign(body, secret) {
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw",
    enc.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sigBytes = await crypto.subtle.sign("HMAC", key, enc.encode(body));
  return Array.from(new Uint8Array(sigBytes))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

Sending the request

bash
BODY='{"status":"up"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$OPSHIFT_SECRET" -hex | awk '{print $2}')
 
curl -X POST https://www.opshift.io/api/ping/your-ping-token \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: $SIG" \
  -d "$BODY"

Empty bodies

Heartbeat pings without a body are accepted, in which case the signature is computed over the empty string. The SDKs handle this automatically.

Secret rotation

Rotating the secret writes a new value and keeps the previous one valid for a grace window. During the overlap, OpShift verifies signatures against either secret — so you can roll the new value through your fleet without downtime.

Failure modes

  • Missing header → 401 Unauthorized with { error: "Missing webhook signature" }.
  • Header present but signature mismatch → 401 Unauthorized.
  • Signature computed over a different body than was sent (e.g. re-serialised JSON with different whitespace) → 401. Always sign and send the same bytes.