{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/python-webhook-verification/python-standard-webhooks","markdown_url":"https://test.abhinandan.one/python-webhook-verification/python-standard-webhooks.md","article":{"id":"299a2625-bedd-45d3-95cb-d2f166cd8821","article_slug":"python-standard-webhooks","parent_article_slug":"python-webhook-verification","parent_article_title":"Verifying Webhook Signatures","kind":"guide","published_at":"2026-08-11T18:55:01.251488+00:00","keywords":["verify_standard_webhooks_signature","sign_standard_webhooks_payload","webhook-signature header","whsec_ secret","handle_webhook_event","Standard Webhooks Python"],"meta_description":"Verifying inbound Primitive webhooks with webhook-id/webhook-timestamp/webhook-signature headers requires verify_standard_webhooks_signature and a whsec_-prefixed secret.","og_image_url":null,"source_file_paths":["sdk-python/src/primitive/webhook.py"],"recording_id":null,"replayable":false,"task_name":"Standard Webhooks Signature Support (Python)","category":"Python SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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.\n\n`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](python-webhook-verification).\n\n<Note>\n\nPrimitive 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.\n\n</Note>\n\n## Verify a Standard Webhooks delivery\n\nCall `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.\n\n<Steps>\n\n<Step title=\"Collect the raw body and the three headers\">\n\nYou need the exact raw request bytes (not re-parsed JSON) plus `webhook-id`, `webhook-timestamp`, and `webhook-signature` from the incoming request.\n\n```python\nraw_body = request.data  # bytes, exactly as received\nmsg_id = request.headers[\"webhook-id\"]\ntimestamp = request.headers[\"webhook-timestamp\"]\nsignature_header = request.headers[\"webhook-signature\"]\n```\n\n</Step>\n\n<Step title=\"Call verify_standard_webhooks_signature\">\n\n```python\nfrom primitive import verify_standard_webhooks_signature, WebhookVerificationError\n\ntry:\n    verify_standard_webhooks_signature(\n        raw_body=raw_body,\n        msg_id=msg_id,\n        timestamp=timestamp,\n        signature_header=signature_header,\n        secret=\"whsec_...\",\n    )\nexcept WebhookVerificationError as error:\n    print(f\"[{error.code}] {error}\")\n    raise\n```\n\n`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.\n\n</Step>\n\n<Step title=\"Handle the result\">\n\nThe function returns `True` on a valid signature and raises `WebhookVerificationError` otherwise. There is no boolean-false return path: either it verifies or it raises.\n\n</Step>\n\n</Steps>\n\n### Error codes\n\n| Code | Raised when |\n|---|---|\n| `MISSING_SECRET` | `secret` is empty, or a string secret isn't valid base64 (with or without the `whsec_` prefix) |\n| `INVALID_SIGNATURE_HEADER` | `timestamp` isn't a unix-seconds integer string, or `signature_header` isn't formatted as `v1,<base64>` |\n| `TIMESTAMP_OUT_OF_RANGE` | The timestamp is more than `tolerance_seconds` old (default 300s / 5 minutes) or more than 60 seconds in the future |\n| `SIGNATURE_MISMATCH` | No signature in the header matches the expected HMAC, most often from a re-serialized body or wrong secret |\n\n<Tip>\n\n`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.\n\n</Tip>\n\n## Sign a Standard Webhooks payload\n\n`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.\n\n```python\nimport json\n\nfrom primitive import sign_standard_webhooks_payload\n\nbody_str = json.dumps({\"event\": \"email.received\"})\n\nresult = sign_standard_webhooks_payload(\n    raw_body=body_str,\n    secret=\"whsec_...\",\n    msg_id=\"msg_2f8b1c\",\n)\n# result == {\"signature\": \"v1,<base64>\", \"msg_id\": \"msg_2f8b1c\", \"timestamp\": 1730000000}\n```\n\nPass an explicit `timestamp` (unix seconds) to pin the signed value instead of using the current time, useful for deterministic tests.\n\n## Verify automatically via handle_webhook_event\n\n`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](python-webhook-events) for the full dispatch flow.\n\n```python\nfrom primitive import handle_webhook_event\n\nevent = handle_webhook_event(\n    body=raw_body,\n    headers=request.headers,\n    secret=\"whsec_...\",\n)\n```\n\nDetection 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.\n\n<Warning>\n\nWhichever 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.\n\n</Warning>\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Verifying Webhook Signatures\" href=\"python-webhook-verification\">\n\nVerify inbound webhooks with the default Primitive-Signature HMAC scheme.\n\n</Card>\n\n<Card title=\"Handling Webhook Events\" href=\"python-webhook-events\">\n\nDispatch every webhook event family with handle_webhook_event and the typed event catalog.\n\n</Card>\n\n<Card title=\"Webhook Events Overview\" href=\"webhook-events\">\n\nUnderstand the shared signature verification contract and event catalog across every SDK.\n\n</Card>\n\n<Card title=\"Python SDK Error Reference\" href=\"python-errors-reference\">\n\nLook up every WebhookVerificationError code and the suggested fix.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-python/src/primitive/webhook.py","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Standard+Webhooks+Signature+Support+%28Python%29&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fpython-standard-webhooks","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}