Skip to content

Stripe Integration ​

Receive and route Stripe webhooks for payments, subscriptions, and other events.

Setup ​

1. Create a Source in WebhookRelay ​

bash
curl -X POST https://api.hookbase.app/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.hookbase.app/ingest/{orgSlug}/stripe

2. Configure Stripe Webhook ​

  1. Go to Stripe Dashboard
  2. Click Add endpoint
  3. Enter your WebhookRelay URL
  4. Select events to receive
  5. Click Add endpoint
  6. Copy the Signing secret (starts with whsec_)
  7. Update your WebhookRelay source with this secret

3. Create Routes ​

bash
# Create destination for your payment handler
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -d '{"name": "Payment Service", "url": "https://api.myapp.com/webhooks/stripe"}'

# Create route
curl -X POST https://api.hookbase.app/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:

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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 ​

javascript
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 ​

javascript
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 ​

javascript
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 ​

json
{
  "name": "Live Mode Only",
  "logic": "and",
  "conditions": [
    {
      "field": "livemode",
      "operator": "equals",
      "value": "true"
    }
  ]
}

Payment Events ​

json
{
  "name": "Payment Events",
  "logic": "or",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "payment_intent"
    },
    {
      "field": "type",
      "operator": "starts_with",
      "value": "charge"
    }
  ]
}

Subscription Events ​

json
{
  "name": "Subscription Events",
  "logic": "and",
  "conditions": [
    {
      "field": "type",
      "operator": "starts_with",
      "value": "customer.subscription"
    }
  ]
}

High-Value Payments ​

json
{
  "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:

bash
# Forward Stripe webhooks to WebhookRelay
stripe listen --forward-to https://api.hookbase.app/ingest/{orgSlug}/stripe-test

# Trigger test events
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created

Test Mode Webhook ​

Create a separate source for test mode:

bash
curl -X POST .../sources \
  -d '{
    "name": "Stripe Test",
    "slug": "stripe-test",
    "verificationConfig": {
      "type": "stripe",
      "secret": "whsec_test_..."
    }
  }'

Best Practices ​

  1. Separate live and test: Use different sources for live and test webhooks

  2. Handle idempotency: Use event.id to prevent duplicate processing

  3. Verify livemode: Always check livemode to avoid processing test events in production

  4. Store event IDs: Log Stripe event IDs for debugging and reconciliation

  5. Handle retries: Stripe retries failed webhooks; ensure your handlers are idempotent

  6. Monitor signatures: Failed signature verifications may indicate misconfigured secrets

Troubleshooting ​

Signature Verification Failed ​

  1. Verify you're using the signing secret (starts with whsec_)
  2. Don't use the API key instead of the webhook secret
  3. Use the correct secret for the environment (live vs test)

Missing Events ​

  1. Check which events are selected in Stripe webhook settings
  2. Verify filters aren't blocking expected events
  3. Check Stripe's webhook logs for delivery attempts

Duplicate Events ​

  1. Stripe may retry failed deliveries
  2. Implement idempotency using event.id
  3. Check for multiple webhook endpoints configured

Released under the MIT License.