HMAC validation
Verify webhook deliveries when Onboard POSTs JSON events to your HTTPS endpoint.
When you configure a webhook in Onboard, Onboard’s systems send an HTTP
POST to your URL whenever matching events occur. This page is for your
backend (the receiver): how to confirm a request actually came from Onboard
before you trust the JSON body.
Onboard includes x-onboard-hmac-sha256 on each delivery. The value is the
hex-encoded HMAC-SHA256 (lowercase hex, no prefix) of the exact bytes of
the JSON POST body your server reads from the request, using your Onboard
company API key (the same key you use for REST Authorization: Token …;
see Authentication) as the HMAC secret.
Steps (on your server)
- Read the raw HTTP body as received (before parsing or re-encoding JSON). Your framework should give you the same bytes Onboard sent (UTF-8).
- Compute HMAC-SHA256 over the raw body bytes using your API key (UTF-8) as the secret, then encode the digest as a lowercase hexadecimal string (no prefix).
- Compare that digest to the
x-onboard-hmac-sha256header value using a constant-time comparison. Reject the request (for example with401) if it does not match, before side effects.
If you parse JSON and then serialize it again, verification often fails: Onboard signed a specific byte sequence at send time; only the raw body you received matches it.
Code examples
These snippets assume you already have the raw body (exact bytes Onboard
POSTed) and the x-onboard-hmac-sha256 header string. Load your API key from a
secret store, not source control. Trim and lowercase the header value before
comparing (Onboard sends lowercase hex; being defensive avoids rare client quirks).
Python
import hashlib
import hmac
def verify_onboard_webhook_signature(
raw_body: bytes,
api_key: str,
header_signature: str,
) -> bool:
expected = hmac.new(
api_key.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, header_signature.strip().lower())Read raw_body before JSON parsing (for example request.body in Django, or
await request.body() in Starlette/FastAPI).
Node.js
import crypto from "node:crypto";
function verifyOnboardWebhookSignature(rawBody, apiKey, headerSignature) {
const expected = crypto
.createHmac("sha256", apiKey)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(String(headerSignature).trim().toLowerCase(), "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Use a Buffer (or Uint8Array) for rawBody with the exact payload bytes.
With Express, use something like express.raw({ type: "application/json" }) on
this route (or read the stream once) so express.json() does not consume the
body before you verify.
PHP
function verify_onboard_webhook_signature(
string $rawBody,
string $apiKey,
string $headerSignature,
): bool {
$expected = hash_hmac('sha256', $rawBody, $apiKey);
return hash_equals($expected, strtolower(trim($headerSignature)));
}Use $rawBody = file_get_contents('php://input') (or equivalent) before the
runtime parses JSON from the same stream.
If verification fails
You do not need to rebuild the JSON yourself when you verify using the raw body as received. If you still get mismatches, common causes are:
- Body changed — Pretty-printing, re-ordering keys, or changing spaces in the JSON string alters the bytes Onboard signed.
- Wrong secret — The HMAC uses your company’s API key, not a webhook-only token or a user’s personal token.
- Wrong bytes — Use the raw request buffer; some frameworks give you a parsed object first, which is too late to recover the original bytes reliably.
The JSON shape and headers are the same ones shown in the Webhooks
overview (envelope fields such as version, created_at, event_uuid,
event_type, target, data, and onboarder_company_id, plus optional
updated_fields inside data for update-style events).
Example header
Each delivery from Onboard to your endpoint includes headers such as:
x-onboard-hmac-sha256: 9f0f2d2f...
x-onboard-account-uuid: 2b4f87e8-4da9-4a79-95f7-0f55a2ea9d4c
x-onboard-webhook-subscription-id: 31
x-onboard-webhook-event-uuid: a2f4bcb0-c4bb-4fbb-b47d-6d4b13db4f3eSee the main Webhooks guide for a full example request and payload shape.
Related
- Webhooks overview — endpoint requirements, payloads, retries
- Security checklist — integration hygiene
How is this guide?