Skip to main content

tai42-webhook-verifier-stripe

License: Apache 2.0

The Stripe webhook-signature verifier plugin for the TAI ecosystem — a per-provider WebhookVerifier that authenticates each inbound Stripe delivery before the platform parses or dispatches its payload.

A webhook door can bind a named verifier to a topic; this plugin supplies the stripe verifier.

Stripe signs every webhook delivery with an HMAC-SHA256 over a payload built from the timestamp, a literal ., and the exact raw request body (str(t).encode() + b"." + body), keyed by the endpoint's signing secret. It sends the timestamp and the resulting digest(s) in the Stripe-Signature header as t=<unix ts>,v1=<hex hmac>, with more than one v1= during a secret rotation. StripeWebhookVerifier recomputes that HMAC over the raw bytes and compares each well-formed v1 in constant time; it also rejects a stale timestamp (replay defense). It returns None on success and raises WebhookVerificationError on any failure.

Its only tai-* dependency is tai42-contract (the interface it registers through); the HMAC work is pure standard library (hmac / hashlib, with time for the freshness clock). It never imports the skeleton — the plugin is contract-facing.

The TAI ecosystem

TAI is an open-source runtime for MCP tools, agents, and workflows. A trigger fires a tool from an inbound event; a webhook verifier is the door's bouncer — the per-provider check that authenticates a delivery before any hook runs. This package is the Stripe verifier. The ecosystem is open-ended: any package can supply a verifier, so this repo is this verifier's own full doc home, and the documentation site covers the platform-level story:

Install

Requires Python 3.13+. Install from PyPI into the environment that runs the server:

uv add tai42-webhook-verifier-stripe

Or from source — clone this repo and add it as an editable dependency; the tai42-* dependencies resolve in-tree from the workspace.

git clone https://github.com/tai42ai/tai42   # next to your app checkout
cd /path/to/your/app
uv add --editable ../tai42/plugins/webhook-verifier-stripe

How it loads — webhook_verifier_modules

The platform loads this plugin through the manifest's webhook_verifier_modules field — the webhook-verifier kind's own declared binding. The host imports each listed module to run its registration side effect. Importing tai42_webhook_verifier_stripe calls tai42_app.webhook_verifiers.register("stripe", StripeWebhookVerifier()), binding the stripe verifier on the app handle.

{
  // `webhook_verifier_modules` is the webhook-verifier kind's own manifest
  // field: the host imports each listed module purely for its registration
  // side effect, binding the named verifier on the app handle.
  "webhook_verifier_modules": ["tai42_webhook_verifier_stripe"]
}

webhook_verifier_modules is the webhook-verifier kind's own declared field: each entry is imported for its registration side effect, so listing tai42_webhook_verifier_stripe registers the stripe verifier at boot.

Configuration — the secret never leaves the environment

A verifier is bound to a topic with {verifier, config}. This plugin's config holds only the name of the environment variable that carries the signing secret — never the secret itself — plus an optional replay tolerance:

{ "verifier": "stripe", "config": { "secret_env": "STRIPE_WEBHOOK_SECRET" } }

config accepts an optional "tolerance_seconds" (a positive int, default 300 — Stripe's documented replay window):

{ "verifier": "stripe", "config": { "secret_env": "STRIPE_WEBHOOK_SECRET", "tolerance_seconds": 300 } }

At verify time the secret is read from os.environ[config["secret_env"]]. A missing env var raises loudly (KeyError) and an empty one raises ValueError; a non-positive or non-int tolerance_seconds also raises ValueError — verification fails CLOSED, so a misconfigured door never becomes a silently-unauthenticated one.

Secret hygiene. The signing secret lives only in an environment variable. Never commit it to a file, a fixture, a manifest, or a URL. Stripe's endpoint signing secret is shown once in the Dashboard as a whsec_… value; store it in the environment, not in the repository.

End-to-end

  1. Create the Stripe webhook endpoint. In the Stripe Dashboard → Developers → Webhooks → Add endpoint, set the endpoint URL to your platform's public door, e.g. https://<your-host>/universal_webhook/stripe-events, and select the events to send. Stripe shows the endpoint's Signing secret (whsec_…) once — store it in the environment as STRIPE_WEBHOOK_SECRET on the platform.

  2. List tai42_webhook_verifier_stripe in the manifest under webhook_verifier_modules (see above) so the stripe verifier is registered at boot.

  3. Bind the verifier to the topic. Through the skeleton's authenticated route:

    curl -X PUT https://<your-host>/api/hooks/topics/stripe-events/verifier \
      -H "Authorization: Bearer $TAI_API_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"verifier": "stripe", "config": {"secret_env": "STRIPE_WEBHOOK_SECRET"}}'
    
  4. Register a hook on the stripe-events topic that runs a demo tool (e.g. a tool that logs the event). Now trigger a Stripe event → Stripe signs and POSTs the delivery → the door verifies the Stripe-Signature HMAC and freshness → the hook fires and the tool runs. A delivery with a missing, malformed, stale, or wrong signature is rejected before any hook runs.

Simulate a signed delivery locally

Compute the signature over t.body with the same secret and POST it — this is what a genuine Stripe delivery looks like on the wire:

# The secret lives only in the environment — never hard-coded here.
export STRIPE_WEBHOOK_SECRET="whsec_example_placeholder"   # your endpoint's signing secret

BODY='{"id":"evt_1","type":"checkout.session.completed"}'
TS="$(date +%s)"
SIG="$(printf '%s' "$TS.$BODY" \
  | openssl dgst -sha256 -hmac "$STRIPE_WEBHOOK_SECRET" -r \
  | cut -d ' ' -f1)"

curl -X POST http://127.0.0.1:8000/universal_webhook/stripe-events \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=$TS,v1=$SIG" \
  --data-raw "$BODY"

printf '%s' (not echo) sends the body with no trailing newline, so the bytes signed match the bytes POSTed exactly — the HMAC is over timestamp + "." + raw body.

Verification rules

verify(body, headers, config) is an async method — the caller awaits it. It raises WebhookVerificationError when:

  • the Stripe-Signature header is missing (lookup is case-insensitive),
  • the header is unparsable: no t= element, a duplicate t=, a non-integer t, or no v1= element at all,
  • the timestamp is stale (now - t > tolerance_seconds) — a future timestamp is accepted by design, so clock skew on the sender is not treated as an attack,
  • no well-formed v1 candidate matches the recomputed digest (the compare uses hmac.compare_digest — constant-time). A malformed v1 (wrong length or non-hex) is skipped, not raised on, so one unusable signature during a secret rotation does not reject a delivery that also carries a good one; a header whose v1 values are all malformed fails here as a plain no-match.

Any non-v1 scheme key (v0=, anything unrecognized) is ignored. A missing secret_env env var raises KeyError, and an empty secret or a non-positive / non-int tolerance_seconds raises ValueError (fails closed), not WebhookVerificationError.

Replay defense. After a delivery passes verification, the verifier claims its Stripe event.id in a seen-set, so a captured signed delivery replayed inside the tolerance window is refused rather than dispatched again. The seen-set TTL runs until the signed freshness window ends (t + tolerance_seconds) — anchored to the signed timestamp and rounded up so an id is never forgotten before that window ends.

Development

uv venv --python 3.13
uv pip install --no-sources --group dev --editable .
uv run --no-sync pytest --cov --cov-report=term-missing
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
uv run --no-sync pyright

License

Apache-2.0. See LICENSE and NOTICE.

Metadata

Release files for tai42-webhook-verifier-stripe 1.3.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tai42-webhook-verifier-stripe 1.3.7
File Size Uploaded
tai42_webhook_verifier_stripe-1.3.7.tar.gz 18.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tai42-webhook-verifier-stripe 1.3.7
File Interpreter ABI Platform
tai42_webhook_verifier_stripe-1.3.7-py3-none-any.whl Python 3 none any Details

Total release size: 35.7 kB

Release files / tai42_webhook_verifier_stripe-1.3.7.tar.gz

Download URL tai42_webhook_verifier_stripe-1.3.7.tar.gz
Size 18.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ebdc1cd8cfc81bcffd23f4ef13528d9739b09e3f94e74d63c074b8c29c8d8fae
BLAKE2b-256 checksum
How to use checksums
93faedefedf150374976b9341cabbfce346612cc10b22b8119715b65ad261ec2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / tai42_webhook_verifier_stripe-1.3.7-py3-none-any.whl

Download URL tai42_webhook_verifier_stripe-1.3.7-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
10216bf646c29ad2a3061daa52ea4eb9b1abd75e3f45ff59e9dcc50e8cb33c24
BLAKE2b-256 checksum
How to use checksums
fbbee07e04dd1c9772152b00b84db0e842e3e0c5f4917fbf67ebc60cbc7cc54f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

1.3.12

2 release files

1.3.11

2 release files

1.3.10

2 release files

1.3.9

2 release files

1.3.8

2 release files

This release

1.3.7 This release

2 release files

1.3.6

2 release files

1.3.5

2 release files

1.3.4

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.13

2 release files

1.2.12

2 release files

1.2.11

2 release files

1.2.10

2 release files

1.2.9

2 release files

1.2.8

2 release files

1.2.7

2 release files

1.2.6

2 release files

1.2.5

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.6

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page