---
title: "Standard Webhooks Signature Support (Python)"
canonical: "https://test.abhinandan.one/python-webhook-verification/python-standard-webhooks"
markdown_url: "https://test.abhinandan.one/python-webhook-verification/python-standard-webhooks.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
parent: "python-webhook-verification"
description: "Verifying inbound Primitive webhooks with webhook-id/webhook-timestamp/webhook-signature headers requires verify_standard_webhooks_signature and a whsec_-prefixed secret."
keywords: ["verify_standard_webhooks_signature", "sign_standard_webhooks_payload", "webhook-signature header", "whsec_ secret", "handle_webhook_event", "Standard Webhooks Python"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:01.251488+00:00"
source_files:
  - "sdk-python/src/primitive/webhook.py"
sections:
  - {anchor: "verify-a-standard-webhooks-delivery", title: "Verify a Standard Webhooks delivery"}
  - {anchor: "step-collect-the-raw-body-and-the-three-headers", title: "Collect the raw body and the three headers"}
  - {anchor: "step-call-verify_standard_webhooks_signature", title: "Call verify_standard_webhooks_signature"}
  - {anchor: "step-handle-the-result", title: "Handle the result"}
  - {anchor: "error-codes", title: "Error codes"}
  - {anchor: "sign-a-standard-webhooks-payload", title: "Sign a Standard Webhooks payload"}
  - {anchor: "verify-automatically-via-handle_webhook_event", title: "Verify automatically via handle_webhook_event"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://www.standardwebhooks.com/) 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](https://test.abhinandan.one/python-webhook-verification.md).

> **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. 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.

```python
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

```python
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. 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

| 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 |

> **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.

```python
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](https://test.abhinandan.one/python-webhook-events.md) for the full dispatch flow.

```python
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.
