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:

The current release line tracks the 7.x contract (tai42-contract>=7,<8).

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

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.2.1
File Size Uploaded
tai42_webhook_verifier_stripe-1.2.1.tar.gz 18.5 kB Details

Built distribution (wheel)

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

Total release size: 35.4 kB

Release files / tai42_webhook_verifier_stripe-1.2.1.tar.gz

Download URL tai42_webhook_verifier_stripe-1.2.1.tar.gz
Size 18.5 kB
Tags Source
SHA-256 checksum
How to use checksums
a3f5cb62b76ba84901348017b24731994d916ecd04a17aa79f7c3f8ccfcddec2
BLAKE2b-256 checksum
How to use checksums
4966b758c7df2f5817b4dcb136c0a98c77534929efcc46e7adaf387d34494d8c
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.2.1-py3-none-any.whl

Download URL tai42_webhook_verifier_stripe-1.2.1-py3-none-any.whl
Size 16.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c3ae5af6c54335d90fb835d430b75112282481fe873687680262a088ee0819c2
BLAKE2b-256 checksum
How to use checksums
594997961bbbbf64f058d98ee5b84a3db42bd7bee96252ffedaea7236c209f87
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

1.3.7

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

This release

1.2.1 This release

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