Signature Verification

Every webhook is signed. Verify the signature before trusting the payload.

The signature header

Every webhook includes a Primitive-Signature header with this format:

t=1734523200,v1=5257a869e7ecebeda32aff1deadbeef951cad7e77a0e56ff536d0ce8e108d8bd
  • t - Unix timestamp when the signature was generated
  • v1 - HMAC-SHA256 signature

Verifying with the SDK

Use these lower-level helpers when you want to inspect the signature before parsing the payload; otherwise reach for the SDK's high-level webhook helper.

import { verifyWebhookSignature, WebhookVerificationError } from '@primitivedotdev/sdk';
try {
verifyWebhookSignature({
rawBody,
signatureHeader: request.headers.get('Primitive-Signature') ?? '',
secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});
// Signature is valid
} catch (error) {
if (error instanceof WebhookVerificationError) {
console.error('Verification failed:', error.code);
}
throw error;
}

Manual verification

If you are not using the SDK, the verification flow is the same in every language:

  1. Parse the header and extract t and v1.
  2. Reject timestamps older than 5 minutes to limit replay attacks.
  3. Compute an HMAC-SHA256 signature of {timestamp}.{rawBody} using your webhook secret.
  4. Compare the provided and expected signatures with a constant-time check (Node crypto.timingSafeEqual, Python hmac.compare_digest, Go hmac.Equal). A regular string compare leaks bytes via timing.
  5. Always use the exact raw request body, never re-serialized JSON.

Standard Webhooks compatibility

Primitive also accepts and emits the Standard Webhooks format (webhook-id, webhook-timestamp, and webhook-signature headers). The SDK transparently verifies whichever style your endpoint receives, so generic Standard Webhooks libraries also work.

Webhook secret

Find it under Settings → Webhooks. Rotate from the same screen; the old value is invalidated immediately, so update handlers first.

Error codes

Verification failures use stable error codes across the SDKs:

CodeDescription
INVALID_SIGNATURE_HEADERMissing or malformed Primitive-Signature header
TIMESTAMP_OUT_OF_RANGETimestamp is too old (possible replay attack)
SIGNATURE_MISMATCHSignature doesn't match expected value
MISSING_SECRETNo webhook secret was provided