Skip to content

Stripe Payment Fan-Out

This use case demonstrates how to route Stripe payment webhooks to multiple destinations simultaneously—billing API, analytics service, and Slack notifications—each with custom transforms and filters.

Architecture

mermaid
flowchart LR
    Stripe[Stripe Webhooks] --> Source[Hookbase Source]
    Source --> R1[Route: Billing]
    Source --> R2[Route: Analytics]
    Source --> R3[Route: Slack]
    R1 --> |Transform| D1[Billing API]
    R2 --> |Transform| D2[Analytics Service]
    R3 --> |Transform| D3[Slack Channel]

Flow:

  1. Stripe sends webhooks to Hookbase source endpoint
  2. Source verifies Stripe signature
  3. Three routes evaluate the event in parallel
  4. Each route applies filters and transforms
  5. Transformed events delivered to respective destinations

Step 1: Create the Source

Create a Stripe source with signature verification:

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-prod",
    "description": "Production Stripe webhooks",
    "verificationConfig": {
      "type": "stripe",
      "secret": "whsec_your_stripe_webhook_signing_secret"
    }
  }'

Response:

json
{
  "data": {
    "id": "src_abc123",
    "name": "Stripe Production",
    "slug": "stripe-prod",
    "url": "https://api.hookbase.app/ingest/your-org/stripe-prod",
    "verificationConfig": {
      "type": "stripe"
    },
    "createdAt": "2026-02-11T10:00:00Z"
  }
}

TIP

Configure this URL in your Stripe Dashboard under Developers → Webhooks → Add endpoint.

Step 2: Create Destinations

Create three destinations for billing, analytics, and Slack:

Billing API Destination

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Billing API",
    "type": "http",
    "config": {
      "url": "https://billing.yourcompany.com/api/webhooks/stripe",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer YOUR_BILLING_API_KEY",
        "Content-Type": "application/json"
      }
    },
    "retryConfig": {
      "maxAttempts": 5,
      "backoffMultiplier": 2,
      "initialInterval": 1000
    }
  }'

Analytics Service Destination

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Analytics Service",
    "type": "http",
    "config": {
      "url": "https://analytics.yourcompany.com/api/events",
      "method": "POST",
      "headers": {
        "X-API-Key": "YOUR_ANALYTICS_API_KEY",
        "Content-Type": "application/json"
      }
    },
    "retryConfig": {
      "maxAttempts": 3,
      "backoffMultiplier": 2,
      "initialInterval": 1000
    }
  }'

Slack Destination

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Slack #payments",
    "type": "slack",
    "config": {
      "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
    },
    "retryConfig": {
      "maxAttempts": 2,
      "backoffMultiplier": 1.5,
      "initialInterval": 500
    }
  }'

Step 3: Create Routes with Filters

Billing Route

Route critical payment events to billing API:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Billing",
    "sourceId": "src_abc123",
    "destinationId": "dst_billing123",
    "filterId": "flt_billing123",
    "transformId": "tfm_billing123",
    "enabled": true
  }'

Filter for critical payment events:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Critical Payment Events",
    "conditions": [
      {
        "field": "type",
        "operator": "in",
        "value": ["checkout.session.completed", "invoice.paid", "invoice.payment_succeeded", "payment_intent.succeeded"]
      }
    ],
    "logic": "AND"
  }'

Analytics Route

Route all payment-related events to analytics:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Analytics",
    "sourceId": "src_abc123",
    "destinationId": "dst_analytics123",
    "filterId": "flt_analytics123",
    "transformId": "tfm_analytics123",
    "enabled": true
  }'

Filter for all payment events:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "All Payment Events",
    "conditions": [
      {
        "field": "type",
        "operator": "matches",
        "value": "^(charge\\.|invoice\\.|payment_intent\\.|checkout\\.session\\.)"
      }
    ],
    "logic": "AND"
  }'

Slack Route

Route failed payments and high-value transactions to Slack:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Slack Alerts",
    "sourceId": "src_abc123",
    "destinationId": "dst_slack123",
    "filterId": "flt_slack123",
    "transformId": "tfm_slack123",
    "enabled": true
  }'

Filter for failures and high-value events:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Failed or High-Value Payments",
    "conditions": [
      {
        "field": "type",
        "operator": "in",
        "value": ["invoice.payment_failed", "charge.failed", "payment_intent.payment_failed"]
      },
      {
        "field": "data.object.amount",
        "operator": "gte",
        "value": 100000
      }
    ],
    "logic": "OR"
  }'

TIP

The amount is in cents. 100000 = $1,000 USD. Adjust based on your needs.

Step 4: Add Transforms

Billing Transform

Extract essential payment data for billing system:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Billing Format",
    "type": "jsonata",
    "config": {
      "expression": "{\n  \"event_type\": type,\n  \"event_id\": id,\n  \"customer_id\": data.object.customer,\n  \"subscription_id\": data.object.subscription,\n  \"amount\": data.object.amount_total ? data.object.amount_total : data.object.amount,\n  \"currency\": data.object.currency,\n  \"status\": data.object.status,\n  \"payment_method\": data.object.payment_method_types ? data.object.payment_method_types[0] : data.object.payment_method,\n  \"invoice_id\": data.object.invoice,\n  \"metadata\": data.object.metadata,\n  \"timestamp\": created * 1000\n}"
    }
  }'

Example Output:

json
{
  "event_type": "checkout.session.completed",
  "event_id": "evt_1234567890",
  "customer_id": "cus_abc123",
  "subscription_id": "sub_xyz789",
  "amount": 4999,
  "currency": "usd",
  "status": "complete",
  "payment_method": "card",
  "invoice_id": "in_def456",
  "metadata": {
    "plan": "pro",
    "annual": "true"
  },
  "timestamp": 1707649200000
}

Analytics Transform

Reshape into analytics event format:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Analytics Format",
    "type": "jsonata",
    "config": {
      "expression": "{\n  \"event_name\": \"payment_\" & $substringAfter(type, \".\"),\n  \"timestamp\": created * 1000,\n  \"user_id\": data.object.customer,\n  \"properties\": {\n    \"amount\": data.object.amount_total ? data.object.amount_total : data.object.amount,\n    \"currency\": data.object.currency,\n    \"status\": data.object.status,\n    \"payment_method\": data.object.payment_method_types ? data.object.payment_method_types[0] : data.object.payment_method,\n    \"subscription_id\": data.object.subscription,\n    \"invoice_id\": data.object.invoice,\n    \"stripe_event_type\": type,\n    \"stripe_event_id\": id\n  },\n  \"context\": {\n    \"source\": \"stripe\",\n    \"version\": api_version\n  }\n}"
    }
  }'

Example Output:

json
{
  "event_name": "payment_session_completed",
  "timestamp": 1707649200000,
  "user_id": "cus_abc123",
  "properties": {
    "amount": 4999,
    "currency": "usd",
    "status": "complete",
    "payment_method": "card",
    "subscription_id": "sub_xyz789",
    "invoice_id": "in_def456",
    "stripe_event_type": "checkout.session.completed",
    "stripe_event_id": "evt_1234567890"
  },
  "context": {
    "source": "stripe",
    "version": "2024-11-20.acacia"
  }
}

Slack Transform

Format as rich Slack Block Kit message:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe → Slack Blocks",
    "type": "jsonata",
    "config": {
      "expression": "(\n  $amount := data.object.amount_total ? data.object.amount_total : data.object.amount;\n  $currency := $uppercase(data.object.currency);\n  $isFailed := $contains(type, \"failed\");\n  $color := $isFailed ? \"#ff0000\" : ($amount >= 100000 ? \"#00ff00\" : \"#cccccc\");\n  $emoji := $isFailed ? \":x:\" : \":moneybag:\";\n  {\n    \"attachments\": [\n      {\n        \"color\": $color,\n        \"blocks\": [\n          {\n            \"type\": \"header\",\n            \"text\": {\n              \"type\": \"plain_text\",\n              \"text\": $emoji & \" \" & ($isFailed ? \"Payment Failed\" : \"Payment Received\")\n            }\n          },\n          {\n            \"type\": \"section\",\n            \"fields\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Amount:*\\n\" & $currency & \" \" & $string($amount / 100)\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Customer:*\\n\" & data.object.customer\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Status:*\\n\" & data.object.status\n              },\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"*Event:*\\n\" & type\n              }\n            ]\n          },\n          {\n            \"type\": \"context\",\n            \"elements\": [\n              {\n                \"type\": \"mrkdwn\",\n                \"text\": \"Event ID: \" & id & \" | <https://dashboard.stripe.com/events/\" & id & \"|View in Stripe>\"\n              }\n            ]\n          }\n        ]\n      }\n    ]\n  }\n)"
    }
  }'

Example Output (Slack Message):

json
{
  "attachments": [
    {
      "color": "#00ff00",
      "blocks": [
        {
          "type": "header",
          "text": {
            "type": "plain_text",
            "text": ":moneybag: Payment Received"
          }
        },
        {
          "type": "section",
          "fields": [
            {
              "type": "mrkdwn",
              "text": "*Amount:*\nUSD 49.99"
            },
            {
              "type": "mrkdwn",
              "text": "*Customer:*\ncus_abc123"
            },
            {
              "type": "mrkdwn",
              "text": "*Status:*\ncomplete"
            },
            {
              "type": "mrkdwn",
              "text": "*Event:*\ncheckout.session.completed"
            }
          ]
        },
        {
          "type": "context",
          "elements": [
            {
              "type": "mrkdwn",
              "text": "Event ID: evt_1234567890 | <https://dashboard.stripe.com/events/evt_1234567890|View in Stripe>"
            }
          ]
        }
      ]
    }
  ]
}

Step 5: Test the Pipeline

Send a test Stripe-like payload to verify routing:

bash
curl -X POST https://api.hookbase.app/ingest/your-org/stripe-prod \
  -H "Content-Type: application/json" \
  -H "Stripe-Signature: t=1234567890,v1=test_signature" \
  -d '{
    "id": "evt_test_123",
    "type": "checkout.session.completed",
    "api_version": "2024-11-20.acacia",
    "created": 1707649200,
    "data": {
      "object": {
        "id": "cs_test_123",
        "customer": "cus_test_abc",
        "subscription": "sub_test_xyz",
        "amount_total": 150000,
        "currency": "usd",
        "status": "complete",
        "payment_method_types": ["card"],
        "invoice": "in_test_def",
        "metadata": {
          "plan": "enterprise",
          "annual": "true"
        }
      }
    }
  }'

TIP

For production testing, use Stripe CLI: stripe trigger checkout.session.completed

Verify Deliveries:

bash
curl https://api.hookbase.app/api/organizations/{orgId}/deliveries?limit=10 \
  -H "Authorization: Bearer YOUR_TOKEN"

You should see 3 deliveries (or 2 if amount filter excluded Slack):

  • Delivery to Billing API with billing format
  • Delivery to Analytics Service with analytics format
  • Delivery to Slack with Block Kit format (if amount >= $1,000 or failed event)

Production Considerations

1. Enable Deduplication

Prevent duplicate event processing if Stripe retries:

bash
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/src_abc123 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "deduplicationConfig": {
      "enabled": true,
      "keyPath": "id",
      "windowSeconds": 86400
    }
  }'

2. Set Up Failover for Billing Route

Ensure critical payment events reach a backup destination:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Billing API Backup",
    "type": "http",
    "config": {
      "url": "https://billing-backup.yourcompany.com/api/webhooks/stripe",
      "method": "POST",
      "headers": {
        "Authorization": "Bearer YOUR_BACKUP_API_KEY",
        "Content-Type": "application/json"
      }
    }
  }'
bash
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/routes/rte_billing123 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "failoverConfig": {
      "enabled": true,
      "destinationId": "dst_backup123",
      "triggerAfterAttempts": 3
    }
  }'

3. Configure Circuit Breaker

Protect downstream services from overload during incidents:

bash
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/destinations/dst_billing123 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "circuitBreakerConfig": {
      "enabled": true,
      "failureThreshold": 10,
      "windowSeconds": 60,
      "resetTimeoutSeconds": 300
    }
  }'

4. Set Up Notification Channels

Get alerted when billing deliveries fail:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/notification-channels \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Engineering Alerts",
    "type": "slack",
    "config": {
      "url": "https://hooks.slack.com/services/YOUR/ALERT/URL"
    },
    "events": ["delivery.failed", "destination.circuit_breaker.opened"],
    "filters": {
      "destinationIds": ["dst_billing123"]
    }
  }'

5. Use Scoped API Keys

Create API keys with limited permissions for external services:

bash
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/api-keys \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe Webhook Ingestion",
    "scopes": ["events:create"],
    "sourceIds": ["src_abc123"],
    "expiresAt": "2027-02-11T00:00:00Z"
  }'

Use the returned API key in your Stripe webhook configuration instead of your user token.

Released under the MIT License.