Standard Webhooks Signature Support (Python)
Verify or sign Primitive webhook deliveries using the Standard Webhooks convention (webhook-id/webhook-timestamp/webhook-signature headers and a whsec_ secret) instead of the default Primitive-Signature HMAC scheme.
Use Standard Webhooks signature support when the receiving side of your integration (a queue, gateway, or third-party tool) already expects the Standard Webhooks convention, webhook-id / webhook-timestamp / webhook-signature headers and a whsec_-prefixed secret, instead of Primitive's default Primitive-Signature: t=<unix-seconds>,v1=<hex> HMAC header.
handle_webhook_event and handle_webhook already detect and verify Standard Webhooks headers automatically; reach for the functions on this page only when you need to verify or sign the format directly, for example writing your own relay or a one-off audit. For the default scheme, see Verifying Webhook Signatures.
Primitive signs every delivery once and sends the same signature value on multiple headers. You don't opt into Standard Webhooks server-side; it's an alternative verification path over the same delivery bytes.
Verify a Standard Webhooks delivery#
Call verify_standard_webhooks_signature, the Python SDK function that checks the webhook-signature header against the raw request body, webhook-id, and webhook-timestamp. It returns True on success and raises WebhookVerificationError on any failure, with a default timestamp tolerance of 300 seconds (5 minutes) in the past and 60 seconds in the future.
- 1
Collect the raw body and the three headers#
You need the exact raw request bytes (not re-parsed JSON) plus
webhook-id,webhook-timestamp, andwebhook-signaturefrom the incoming request.raw_body = request.data # bytes, exactly as received msg_id = request.headers["webhook-id"] timestamp = request.headers["webhook-timestamp"] signature_header = request.headers["webhook-signature"] - 2
Call verify_standard_webhooks_signature#
from primitive import verify_standard_webhooks_signature, WebhookVerificationError try: verify_standard_webhooks_signature( raw_body=raw_body, msg_id=msg_id, timestamp=timestamp, signature_header=signature_header, secret="whsec_...", ) except WebhookVerificationError as error: print(f"[{error.code}] {error}") raisesecretaccepts thewhsec_-prefixed base64 secret as given, or raw bytes. The function strips thewhsec_prefix and base64-decodes the rest internally. Passtolerance_secondsto override the 300-second replay window. - 3
Handle the result#
The function returns
Trueon a valid signature and raisesWebhookVerificationErrorotherwise. There is no boolean-false return path: either it verifies or it raises.
Error codes#
| Code | Raised when |
|---|---|
MISSING_SECRET | secret is empty, or a string secret isn't valid base64 (with or without the whsec_ prefix) |
INVALID_SIGNATURE_HEADER | timestamp isn't a unix-seconds integer string, or signature_header isn't formatted as v1,<base64> |
TIMESTAMP_OUT_OF_RANGE | The timestamp is more than tolerance_seconds old (default 300s / 5 minutes) or more than 60 seconds in the future |
SIGNATURE_MISMATCH | No signature in the header matches the expected HMAC, most often from a re-serialized body or wrong secret |
signature_header can carry multiple space-separated v1,<base64> values (Standard Webhooks supports key rotation with multiple valid signatures). Verification succeeds if any one of them matches.
Sign a Standard Webhooks payload#
sign_standard_webhooks_payload produces a Standard Webhooks-compatible signature from a body, a secret, and a message id, for cases where your own code relays a Primitive event onward to a system that verifies this format.
import json
from primitive import sign_standard_webhooks_payload
body_str = json.dumps({"event": "email.received"})
result = sign_standard_webhooks_payload(
raw_body=body_str,
secret="whsec_...",
msg_id="msg_2f8b1c",
)
# result == {"signature": "v1,<base64>", "msg_id": "msg_2f8b1c", "timestamp": 1730000000}
Pass an explicit timestamp (unix seconds) to pin the signed value instead of using the current time, useful for deterministic tests.
Verify automatically via handle_webhook_event#
handle_webhook_event (and the legacy handle_webhook) detect Standard Webhooks headers on an inbound request and verify with the right scheme automatically, so you never call verify_standard_webhooks_signature yourself unless you're bypassing those entry points. See Handling Webhook Events for the full dispatch flow.
from primitive import handle_webhook_event
event = handle_webhook_event(
body=raw_body,
headers=request.headers,
secret="whsec_...",
)
Detection rule: if a webhook-signature header is present, Standard Webhooks verification runs; otherwise the SDK falls back to the default Primitive-Signature HMAC path. A webhook-signature header present without webhook-id or webhook-timestamp raises WebhookVerificationError with code INVALID_SIGNATURE_HEADER rather than silently falling back: a partial header set means a misconfiguration, not a Primitive-format delivery.
Whichever scheme you verify with, always sign over the raw request body. Re-serializing JSON before verification (via json.dumps after json.loads) changes whitespace and key order and produces a SIGNATURE_MISMATCH, even with the correct secret.
Next steps#
Verify inbound webhooks with the default Primitive-Signature HMAC scheme.
Handling Webhook EventsDispatch every webhook event family with handle_webhook_event and the typed event catalog.
Webhook Events OverviewUnderstand the shared signature verification contract and event catalog across every SDK.
Python SDK Error ReferenceLook up every WebhookVerificationError code and the suggested fix.
Was this page helpful?