Stripe Integration
Receive and route Stripe webhooks for payments, subscriptions, and other events.
Setup
1. Create a Source in WebhookRelay
curl -X POST https://api.webhookrelay.com/api/organizations/{orgId}/sources \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe Production",
"slug": "stripe",
"verificationConfig": {
"type": "stripe",
"secret": "whsec_..."
}
}'Save your webhook URL:
https://api.webhookrelay.com/ingest/{orgSlug}/stripe2. Configure Stripe Webhook
- Go to Stripe Dashboard
- Click Add endpoint
- Enter your WebhookRelay URL
- Select events to receive
- Click Add endpoint
- Copy the Signing secret (starts with
whsec_) - Update your WebhookRelay source with this secret
3. Create Routes
# Create destination for your payment handler
curl -X POST https://api.webhookrelay.com/api/organizations/{orgId}/destinations \
-d '{"name": "Payment Service", "url": "https://api.myapp.com/webhooks/stripe"}'
# Create route
curl -X POST https://api.webhookrelay.com/api/organizations/{orgId}/routes \
-d '{"name": "Stripe to Payment Service", "sourceId": "src_...", "destinationIds": ["dst_..."]}'Signature Verification
Stripe uses a custom signature scheme with timestamps to prevent replay attacks.
The signature is sent in the Stripe-Signature header:
Stripe-Signature: t=1492774577,v1=5257a869...WebhookRelay automatically verifies this when configured:
{
"verificationConfig": {
"type": "stripe",
"secret": "whsec_..."
}
}Timestamp Tolerance
Stripe signatures include a timestamp. WebhookRelay rejects webhooks older than 5 minutes by default to prevent replay attacks.
Common Events
payment_intent.succeeded
Triggered when a payment is successful.
{
"id": "evt_1234567890",
"object": "event",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_1234567890",
"object": "payment_intent",
"amount": 2000,
"currency": "usd",
"status": "succeeded",
"customer": "cus_1234567890",
"metadata": {
"order_id": "12345"
}
}
},
"livemode": true,
"created": 1234567890
}customer.subscription.created
Triggered when a subscription is created.
{
"id": "evt_1234567890",
"object": "event",
"type": "customer.subscription.created",
"data": {
"object": {
"id": "sub_1234567890",
"object": "subscription",
"customer": "cus_1234567890",
"status": "active",
"items": {
"data": [{
"price": {
"id": "price_1234567890",
"product": "prod_1234567890"
}
}]
},
"current_period_start": 1234567890,
"current_period_end": 1237246290
}
},
"livemode": true
}invoice.payment_failed
Triggered when a payment fails.
{
"id": "evt_1234567890",
"object": "event",
"type": "invoice.payment_failed",
"data": {
"object": {
"id": "in_1234567890",
"object": "invoice",
"customer": "cus_1234567890",
"subscription": "sub_1234567890",
"amount_due": 2000,
"attempt_count": 1,
"next_payment_attempt": 1234657890
}
}
}Transform Examples
Slack Alert for Payment Success
function transform(payload) {
const payment = payload.data.object;
const amount = (payment.amount / 100).toFixed(2);
const currency = payment.currency.toUpperCase();
return {
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "Payment Received"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Amount:*\n${currency} ${amount}`
},
{
type: "mrkdwn",
text: `*Customer:*\n${payment.customer}`
}
]
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `Payment ID: ${payment.id}`
}
]
}
]
};
}Database Format
function transform(payload) {
const event = payload;
const data = event.data.object;
return {
stripe_event_id: event.id,
event_type: event.type,
object_id: data.id,
object_type: data.object,
customer_id: data.customer,
amount: data.amount,
currency: data.currency,
status: data.status,
livemode: event.livemode,
created_at: new Date(event.created * 1000).toISOString(),
metadata: data.metadata || {}
};
}Alert for Failed Payments
function transform(payload) {
const invoice = payload.data.object;
const amount = (invoice.amount_due / 100).toFixed(2);
return {
channel: "#billing-alerts",
username: "Stripe Bot",
icon_emoji: ":warning:",
attachments: [{
color: "danger",
title: "Payment Failed",
fields: [
{ title: "Customer", value: invoice.customer, short: true },
{ title: "Amount", value: `$${amount}`, short: true },
{ title: "Attempts", value: invoice.attempt_count.toString(), short: true },
{ title: "Subscription", value: invoice.subscription || "N/A", short: true }
],
footer: `Invoice ${invoice.id}`,
ts: Math.floor(Date.now() / 1000)
}]
};
}Filter Examples
Production Events Only
{
"name": "Live Mode Only",
"logic": "and",
"conditions": [
{
"field": "livemode",
"operator": "equals",
"value": "true"
}
]
}Payment Events
{
"name": "Payment Events",
"logic": "or",
"conditions": [
{
"field": "type",
"operator": "starts_with",
"value": "payment_intent"
},
{
"field": "type",
"operator": "starts_with",
"value": "charge"
}
]
}Subscription Events
{
"name": "Subscription Events",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "starts_with",
"value": "customer.subscription"
}
]
}High-Value Payments
{
"name": "High Value ($100+)",
"logic": "and",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "payment_intent.succeeded"
},
{
"field": "data.object.amount",
"operator": "greater_than",
"value": "10000"
}
]
}Testing
Using Stripe CLI
Test locally with the Stripe CLI:
# Forward Stripe webhooks to WebhookRelay
stripe listen --forward-to https://api.webhookrelay.com/ingest/{orgSlug}/stripe-test
# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.createdTest Mode Webhook
Create a separate source for test mode:
curl -X POST .../sources \
-d '{
"name": "Stripe Test",
"slug": "stripe-test",
"verificationConfig": {
"type": "stripe",
"secret": "whsec_test_..."
}
}'Best Practices
Separate live and test: Use different sources for live and test webhooks
Handle idempotency: Use
event.idto prevent duplicate processingVerify livemode: Always check
livemodeto avoid processing test events in productionStore event IDs: Log Stripe event IDs for debugging and reconciliation
Handle retries: Stripe retries failed webhooks; ensure your handlers are idempotent
Monitor signatures: Failed signature verifications may indicate misconfigured secrets
Troubleshooting
Signature Verification Failed
- Verify you're using the signing secret (starts with
whsec_) - Don't use the API key instead of the webhook secret
- Use the correct secret for the environment (live vs test)
Missing Events
- Check which events are selected in Stripe webhook settings
- Verify filters aren't blocking expected events
- Check Stripe's webhook logs for delivery attempts
Duplicate Events
- Stripe may retry failed deliveries
- Implement idempotency using
event.id - Check for multiple webhook endpoints configured