Signature Verification
Primitive signs outbound webhook deliveries so your endpoint can verify that the payload came from Primitive and was not modified in transit.
The header is:
Primitive-Signature: t=<unix-seconds>,v1=<hex-hmac>
Verify the exact raw request body. Do not parse JSON and re-stringify it before verification.
SDK Helpers
import {
verifyWebhookSignature,
WebhookVerificationError,
PRIMITIVE_SIGNATURE_HEADER,
} from '@primitivedotdev/sdk/api';
export async function POST(req: Request) {
const rawBody = await req.text();
const signatureHeader = req.headers.get(PRIMITIVE_SIGNATURE_HEADER) ?? '';
try {
await verifyWebhookSignature({
rawBody,
signatureHeader,
secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});
} catch (error) {
if (error instanceof WebhookVerificationError) {
return new Response('invalid signature', { status: 401 });
}
throw error;
}
return Response.json({ ok: true });
}import os
from primitive import WebhookVerificationError, verify_webhook_signature
async def handle(request):
raw_body = await request.body()
signature_header = request.headers.get('Primitive-Signature', '')
try:
verify_webhook_signature(
raw_body=raw_body,
signature_header=signature_header,
secret=os.environ['PRIMITIVE_WEBHOOK_SECRET'],
)
except WebhookVerificationError:
return {'status': 401, 'body': 'invalid signature'}
return {'ok': True}body, _ := io.ReadAll(r.Body)
signatureHeader := r.Header.Get("Primitive-Signature")
_, err := primitive.VerifyWebhookSignature(primitive.VerifyOptions{
RawBody: body,
SignatureHeader: signatureHeader,
Secret: os.Getenv("PRIMITIVE_WEBHOOK_SECRET"),
})
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
Manual Flow
- Parse the
t and v1 parts of Primitive-Signature.
- Reject timestamps outside your tolerance window.
- Build the signed payload as
<timestamp>.<raw-body>.
- Compute HMAC-SHA256 with your webhook signing secret.
- Compare the expected hex digest to
v1 using a constant-time comparison.
Common Mistakes
- Reading
request.json() before verification.
- Verifying a re-serialized JSON string instead of raw bytes.
- Comparing strings with normal equality instead of a timing-safe comparison.
- Returning 4xx for event types you intentionally ignore. Verify first, then return 2xx for ignored but trusted events.
Related Pages