Quick Use
- Add a Stripe webhook endpoint at dashboard.stripe.com/webhooks pointing at /stripe/webhook
- Set STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, ANTHROPIC_API_KEY
- Map events to tool names in EVENT_TO_TOOL
Intro
This skill turns Stripe webhook events into Anthropic tool-call inputs so a Claude agent reacts to real payment lifecycle events: payment succeeded, dispute opened, subscription canceled, refund issued. The pattern is a tiny FastAPI/Express endpoint that verifies the Stripe signature, transforms the event into a structured tool call, and dispatches the call against your existing agent. Best for: support agents that triage disputes, billing chatbots that confirm payments, churn-intervention agents reacting to subscription events. Works with: Anthropic API, OpenAI Tool Use, any agent that takes tool inputs. Setup time: 15 minutes.
FastAPI endpoint
import os, stripe
from anthropic import Anthropic
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]
claude = Anthropic()
EVENT_TO_TOOL = {
"charge.dispute.created": "handle_dispute",
"invoice.payment_failed": "retry_failed_payment",
"customer.subscription.deleted": "churn_outreach",
"charge.refunded": "log_refund",
}
@app.post("/stripe/webhook")
async def webhook(request: Request):
payload = await request.body()
sig = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(payload, sig, WEBHOOK_SECRET)
except (ValueError, stripe.error.SignatureVerificationError):
raise HTTPException(400, "bad signature")
tool_name = EVENT_TO_TOOL.get(event["type"])
if not tool_name:
return {"status": "ignored"}
obj = event["data"]["object"]
msg = claude.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=[{
"name": tool_name,
"description": f"Triggered by Stripe event {event['type']}",
"input_schema": {"type": "object", "properties": {
"stripe_id": {"type": "string"},
"amount": {"type": "integer"},
"customer": {"type": "string"},
"reason": {"type": "string"},
}},
}],
messages=[{
"role": "user",
"content": f"A Stripe {event['type']} event occurred. Decide on next action.\n\n{obj}",
}],
)
return {"status": "dispatched", "claude_response": msg.content}Production hardening
- Signature verification is mandatory — never skip
construct_event. Without it, anyone can POST fake events. - Idempotency — Stripe retries webhooks for 3 days on non-2xx. Store
event["id"]in Redis with 7-day TTL; skip if seen. - Async dispatch — return 200 within 10s. Push the Claude call to a queue (Celery / Inngest / SQS) if it takes longer.
- Replay attacks —
construct_eventenforces 5-minute timestamp tolerance. Don't widen it.
FAQ
Q: Why not have Claude call Stripe directly with the Agent Toolkit? A: For synchronous user-driven flows, do that. This pattern is for async events that happen without a user prompt — disputes opened by the cardholder, subscriptions canceled by Stripe Smart Retries, payments failing on retry. The webhook is the trigger; Claude is the policy engine.
Q: How do I scope which events fire Claude? A: Whitelist in the EVENT_TO_TOOL map. Stripe will keep delivering all subscribed events but you only spend Claude tokens on the ones you map. Subscribe selectively at dashboard.stripe.com/webhooks too.
Q: What about test mode?
A: Use Stripe CLI: stripe trigger charge.dispute.created --forward-to localhost:8000/stripe/webhook. The CLI signs events with your test webhook secret and replays them locally.
Source & Thanks
Pattern compiled from Stripe Webhooks + Anthropic Tool Use.
Stripe MIT-licensed, Anthropic SDK MIT-licensed.