Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /python-webhook-verification/python-standard-webhooks.md.
Verified · 8/11/2026

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.

Note

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. 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, and webhook-signature from 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. 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}")
        raise
    

    secret accepts the whsec_-prefixed base64 secret as given, or raw bytes. The function strips the whsec_ prefix and base64-decodes the rest internally. Pass tolerance_seconds to override the 300-second replay window.

  3. 3

    Handle the result#

    The function returns True on a valid signature and raises WebhookVerificationError otherwise. There is no boolean-false return path: either it verifies or it raises.

Error codes#

CodeRaised when
MISSING_SECRETsecret is empty, or a string secret isn't valid base64 (with or without the whsec_ prefix)
INVALID_SIGNATURE_HEADERtimestamp isn't a unix-seconds integer string, or signature_header isn't formatted as v1,<base64>
TIMESTAMP_OUT_OF_RANGEThe timestamp is more than tolerance_seconds old (default 300s / 5 minutes) or more than 60 seconds in the future
SIGNATURE_MISMATCHNo signature in the header matches the expected HMAC, most often from a re-serialized body or wrong secret
Tip

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.

Warning

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#

Was this page helpful?

© Primitive SDKs

Powered by Browzer