Multi-Payment Processor Consolidation
Consolidate transaction webhooks from Stripe, Paddle, and Square into a unified format. Events are normalized and routed to a central finance system, with high-value transactions triggering Slack alerts. This fan-in pattern with content-based routing is essential for businesses accepting payments through multiple processors.
Architecture
flowchart LR
Stripe[Stripe Webhooks] --> S1[Source: Stripe]
Paddle[Paddle Webhooks] --> S2[Source: Paddle]
Square[Square Webhooks] --> S3[Source: Square]
S1 --> R1[Route: Stripe → Finance]
S1 --> R2[Route: Stripe → High-Value Alerts]
S2 --> R3[Route: Paddle → Finance]
S2 --> R4[Route: Paddle → High-Value Alerts]
S3 --> R5[Route: Square → Finance]
S3 --> R6[Route: Square → High-Value Alerts]
R1 --> |Transform| D1[Finance API]
R3 --> |Transform| D1
R5 --> |Transform| D1
R2 --> |Transform| D2[Slack #high-value]
R4 --> |Transform| D2
R6 --> |Transform| D2Flow:
- Three payment processors send webhooks to their respective Hookbase source endpoints
- Each source verifies the provider-specific signature
- Routes evaluate events using content-based filters (event type, amount thresholds)
- Transforms normalize each provider's payload into a unified transaction schema
- Finance system receives consistent records; Slack receives high-value alerts
Key challenge: Stripe uses amounts in cents, Paddle reports totals with tax included as a Merchant of Record (MoR), and Square nests amounts inside a money object. The transforms normalize all of this.
Step 1: Create Sources
Create one source per payment processor with signature verification.
Stripe Source
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-payments",
"description": "Stripe payment webhooks for consolidation",
"provider": "stripe",
"signingSecret": "whsec_your_stripe_webhook_signing_secret"
}'Response:
{
"data": {
"id": "11111111-1111-4111-8111-111111110015",
"name": "Stripe Production",
"slug": "stripe-payments",
"url": "https://api.hookbase.app/ingest/your-org/stripe-payments",
"provider": "stripe",
"createdAt": "2026-03-01T10:00:00Z"
}
}Paddle Source
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle Production",
"slug": "paddle-payments",
"description": "Paddle payment webhooks for consolidation",
"provider": "paddle",
"signingSecret": "pdl_your_paddle_webhook_secret"
}'Response:
{
"data": {
"id": "11111111-1111-4111-8111-111111110013",
"name": "Paddle Production",
"slug": "paddle-payments",
"url": "https://api.hookbase.app/ingest/your-org/paddle-payments",
"provider": "paddle",
"createdAt": "2026-03-01T10:01:00Z"
}
}Tip
Paddle is a Merchant of Record (MoR). Its payloads include tax, fees, and payout details that differ from direct processors. The transforms in Step 4 account for this.
Square Source
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/sources \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square Production",
"slug": "square-payments",
"description": "Square payment webhooks for consolidation"
}'Warning
Hookbase does not verify Square signatures, so this source carries no provider. Events arrive with signature_valid: false and are still delivered; leave rejectInvalidSignatures off or every Square webhook is dropped. Square signs the notification URL together with the body. See the Square integration guide for why and what to do instead.
Response:
{
"data": {
"id": "11111111-1111-4111-8111-111111110014",
"name": "Square Production",
"slug": "square-payments",
"url": "https://api.hookbase.app/ingest/your-org/square-payments",
"provider": null,
"createdAt": "2026-03-01T10:02:00Z"
}
}Step 2: Create Destinations
Finance API Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance API",
"slug": "finance-api",
"type": "http",
"url": "https://finance.yourcompany.com/api/transactions",
"method": "POST",
"headers": {
"Authorization": "Bearer YOUR_FINANCE_API_KEY",
"Content-Type": "application/json"
}
}'Slack High-Value Alerts Destination
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack #high-value-payments",
"slug": "slack-high-value-payments",
"type": "http",
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
}'Step 3: Create Routes with Filters
Each source needs two routes: one to the finance system (successful transactions) and one to Slack (high-value only). Filters use event types and amount thresholds for content-based routing.
Stripe Routes
Filter for successful Stripe payments:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe Successful Payments",
"conditions": [
{
"field": "type",
"operator": "in",
"value": ["checkout.session.completed", "payment_intent.succeeded", "invoice.paid", "charge.succeeded"]
}
],
"logic": "AND"
}'Route: Stripe to Finance:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe → Finance",
"sourceId": "11111111-1111-4111-8111-111111110015",
"destinationId": "22222222-2222-4222-8222-222222220010",
"filterId": "77777777-7777-4777-8777-77777777001c",
"transformId": "88888888-8888-4888-8888-88888888002a",
"isActive": true
}'Filter for high-value Stripe payments ($500+):
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe High-Value Payments",
"conditions": [
{
"field": "type",
"operator": "in",
"value": ["checkout.session.completed", "payment_intent.succeeded", "charge.succeeded"]
},
{
"field": "data.object.amount",
"operator": "gte",
"value": 50000
}
],
"logic": "AND"
}'Tip
Stripe amounts are in the smallest currency unit (cents for USD). 50000 = $500.00. Adjust the threshold based on your transaction volume and what you consider "high-value."
Route: Stripe to Slack Alerts:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe → High-Value Alerts",
"sourceId": "11111111-1111-4111-8111-111111110015",
"destinationId": "22222222-2222-4222-8222-22222222000d",
"filterId": "77777777-7777-4777-8777-77777777001d",
"transformId": "88888888-8888-4888-8888-88888888002b",
"isActive": true
}'Paddle Routes
Filter for successful Paddle transactions:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle Successful Transactions",
"conditions": [
{
"field": "event_type",
"operator": "in",
"value": ["transaction.completed", "transaction.paid"]
}
],
"logic": "AND"
}'Route: Paddle to Finance:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle → Finance",
"sourceId": "11111111-1111-4111-8111-111111110013",
"destinationId": "22222222-2222-4222-8222-222222220010",
"filterId": "77777777-7777-4777-8777-777777770014",
"transformId": "88888888-8888-4888-8888-888888880015",
"isActive": true
}'Filter for high-value Paddle transactions ($500+):
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle High-Value Transactions",
"conditions": [
{
"field": "event_type",
"operator": "in",
"value": ["transaction.completed", "transaction.paid"]
},
{
"field": "data.details.totals.grand_total",
"operator": "gte",
"value": "500.00"
}
],
"logic": "AND"
}'Route: Paddle to Slack Alerts:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle → High-Value Alerts",
"sourceId": "11111111-1111-4111-8111-111111110013",
"destinationId": "22222222-2222-4222-8222-22222222000d",
"filterId": "77777777-7777-4777-8777-777777770015",
"transformId": "88888888-8888-4888-8888-888888880016",
"isActive": true
}'Square Routes
Filter for successful Square payments:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square Successful Payments",
"conditions": [
{
"field": "type",
"operator": "in",
"value": ["payment.completed", "payment.updated"]
},
{
"field": "data.object.payment.status",
"operator": "eq",
"value": "COMPLETED"
}
],
"logic": "AND"
}'Route: Square to Finance:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square → Finance",
"sourceId": "11111111-1111-4111-8111-111111110014",
"destinationId": "22222222-2222-4222-8222-222222220010",
"filterId": "77777777-7777-4777-8777-77777777001a",
"transformId": "88888888-8888-4888-8888-888888880026",
"isActive": true
}'Filter for high-value Square payments ($500+):
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/filters \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square High-Value Payments",
"conditions": [
{
"field": "type",
"operator": "in",
"value": ["payment.completed", "payment.updated"]
},
{
"field": "data.object.payment.status",
"operator": "eq",
"value": "COMPLETED"
},
{
"field": "data.object.payment.amount_money.amount",
"operator": "gte",
"value": 50000
}
],
"logic": "AND"
}'Tip
Square amounts are also in the smallest currency unit (cents for USD), matching Stripe's convention. The filter threshold of 50000 equals $500.00.
Route: Square to Slack Alerts:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square → High-Value Alerts",
"sourceId": "11111111-1111-4111-8111-111111110014",
"destinationId": "22222222-2222-4222-8222-22222222000d",
"filterId": "77777777-7777-4777-8777-77777777001b",
"transformId": "88888888-8888-4888-8888-888888880027",
"isActive": true
}'Step 4: Add Transforms
Each payment processor uses a different payload structure. The transforms normalize everything into a unified transaction schema that the finance system expects.
All three finance transforms output a unified schema with consistent fields: transaction_id, provider, amount_cents, currency, tax_cents, net_amount_cents, fee_cents, is_merchant_of_record, and timestamp. This lets your finance system process transactions identically regardless of origin.
Stripe Finance Transform
Normalize Stripe payment events into the unified schema:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe → Unified Transaction",
"type": "jsonata",
"config": {
"expression": "(\n $amount := data.object.amount_total ? data.object.amount_total : data.object.amount;\n $currency := $uppercase(data.object.currency);\n {\n \"transaction_id\": \"str_\" & data.object.id,\n \"provider\": \"stripe\",\n \"provider_event_id\": id,\n \"customer_id\": data.object.customer,\n \"amount_cents\": $amount,\n \"currency\": $currency,\n \"amount_display\": $string($amount / 100),\n \"tax_cents\": data.object.total_details.amount_tax ? data.object.total_details.amount_tax : 0,\n \"net_amount_cents\": $amount - (data.object.total_details.amount_tax ? data.object.total_details.amount_tax : 0),\n \"fee_cents\": 0,\n \"is_merchant_of_record\": false,\n \"payment_method\": data.object.payment_method_types ? data.object.payment_method_types[0] : \"card\",\n \"status\": \"succeeded\",\n \"provider_event_type\": type,\n \"metadata\": data.object.metadata ? data.object.metadata : {},\n \"timestamp\": $fromMillis(created * 1000)\n }\n)"
}
}'Example Output (from a Stripe checkout.session.completed):
{
"transaction_id": "str_cs_abc123",
"provider": "stripe",
"provider_event_id": "evt_1234567890",
"customer_id": "cus_abc123",
"amount_cents": 14999,
"currency": "USD",
"amount_display": "149.99",
"tax_cents": 1200,
"net_amount_cents": 13799,
"fee_cents": 0,
"is_merchant_of_record": false,
"payment_method": "card",
"status": "succeeded",
"provider_event_type": "checkout.session.completed",
"metadata": {
"plan": "pro",
"annual": "true"
},
"timestamp": "2026-03-01T10:00:00.000Z"
}Paddle Finance Transform
Paddle is a Merchant of Record — it collects payments, handles taxes, and remits a payout. The transform extracts net earnings alongside the gross total:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle → Unified Transaction",
"type": "jsonata",
"config": {
"expression": "(\n $total := $number(data.details.totals.grand_total);\n $tax := $number(data.details.totals.tax);\n $fee := $number(data.details.totals.fee ? data.details.totals.fee : \"0\");\n $earnings := $number(data.details.totals.earnings ? data.details.totals.earnings : $string($total - $tax - $fee));\n $currency := $uppercase(data.currency_code);\n {\n \"transaction_id\": \"pdl_\" & data.id,\n \"provider\": \"paddle\",\n \"provider_event_id\": event_id,\n \"customer_id\": data.customer_id,\n \"amount_cents\": $round($total * 100),\n \"currency\": $currency,\n \"amount_display\": $string($total),\n \"tax_cents\": $round($tax * 100),\n \"net_amount_cents\": $round($earnings * 100),\n \"fee_cents\": $round($fee * 100),\n \"is_merchant_of_record\": true,\n \"payment_method\": data.payments[0].method_details.type ? data.payments[0].method_details.type : \"unknown\",\n \"status\": \"succeeded\",\n \"provider_event_type\": event_type,\n \"metadata\": data.custom_data ? data.custom_data : {},\n \"timestamp\": occurred_at\n }\n)"
}
}'Warning
Paddle reports amounts as decimal strings (e.g., "149.99") rather than cents. The transform converts using $round($total * 100) to avoid floating-point issues. The is_merchant_of_record: true flag lets your finance system distinguish MoR transactions.
Example Output (from a Paddle transaction.completed):
{
"transaction_id": "pdl_txn_01h8bxpvx2T9XYZ",
"provider": "paddle",
"provider_event_id": "ntf_01h8bxqyz3ABC",
"customer_id": "ctm_01h7mrr5j8DEF",
"amount_cents": 14999,
"currency": "USD",
"amount_display": "149.99",
"tax_cents": 2400,
"net_amount_cents": 11249,
"fee_cents": 1350,
"is_merchant_of_record": true,
"payment_method": "card",
"status": "succeeded",
"provider_event_type": "transaction.completed",
"metadata": {
"plan": "pro",
"source": "website"
},
"timestamp": "2026-03-01T10:05:00.000000Z"
}Square Finance Transform
Square nests amounts inside a money object with separate amount and currency fields:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square → Unified Transaction",
"type": "jsonata",
"config": {
"expression": "(\n $payment := data.object.payment;\n $amount := $payment.amount_money.amount;\n $currency := $uppercase($payment.amount_money.currency);\n $tip := $payment.tip_money.amount ? $payment.tip_money.amount : 0;\n $fee := $payment.processing_fee ? $sum($payment.processing_fee.amount_money.amount) : 0;\n {\n \"transaction_id\": \"sqr_\" & $payment.id,\n \"provider\": \"square\",\n \"provider_event_id\": event_id ? event_id : $payment.id,\n \"customer_id\": $payment.customer_id ? $payment.customer_id : \"anonymous\",\n \"amount_cents\": $amount,\n \"currency\": $currency,\n \"amount_display\": $string($amount / 100),\n \"tax_cents\": $payment.tax_money.amount ? $payment.tax_money.amount : 0,\n \"net_amount_cents\": $amount - $fee,\n \"fee_cents\": $fee,\n \"is_merchant_of_record\": false,\n \"payment_method\": $payment.source_type ? $lowercase($payment.source_type) : \"card\",\n \"status\": \"succeeded\",\n \"provider_event_type\": type,\n \"metadata\": {\n \"location_id\": $payment.location_id,\n \"tip_cents\": $tip\n },\n \"timestamp\": $payment.created_at\n }\n)"
}
}'Example Output (from a Square payment.completed):
{
"transaction_id": "sqr_KkAkhdMYPBJNgD",
"provider": "square",
"provider_event_id": "evt_sq_abc123",
"customer_id": "VDKXEEKPJN48M",
"amount_cents": 15500,
"currency": "USD",
"amount_display": "155",
"tax_cents": 1200,
"net_amount_cents": 15050,
"fee_cents": 450,
"is_merchant_of_record": false,
"payment_method": "card",
"status": "succeeded",
"provider_event_type": "payment.completed",
"metadata": {
"location_id": "LPNKH7GXR3KH8",
"tip_cents": 500
},
"timestamp": "2026-03-01T10:10:00.000Z"
}Slack High-Value Alert Transforms
Each processor needs its own Slack transform to extract the correct amount fields. All produce the same Block Kit structure.
Stripe High-Value Slack Transform:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Stripe → High-Value Slack Alert",
"type": "jsonata",
"config": {
"expression": "(\n $amount := data.object.amount_total ? data.object.amount_total : data.object.amount;\n $currency := $uppercase(data.object.currency);\n {\n \"attachments\": [\n {\n \"color\": \"#635bff\",\n \"blocks\": [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \":moneybag: High-Value Payment — Stripe\"\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\": \"*Payment Method:*\\n\" & (data.object.payment_method_types ? data.object.payment_method_types[0] : \"card\")\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\": \"Provider: Stripe | Event: \" & id & \" | <https://dashboard.stripe.com/events/\" & id & \"|View in Stripe>\"\n }\n ]\n }\n ]\n }\n ]\n }\n)"
}
}'Paddle High-Value Slack Transform:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Paddle → High-Value Slack Alert",
"type": "jsonata",
"config": {
"expression": "(\n $total := data.details.totals.grand_total;\n $currency := $uppercase(data.currency_code);\n $earnings := data.details.totals.earnings ? data.details.totals.earnings : $total;\n {\n \"attachments\": [\n {\n \"color\": \"#3b82f6\",\n \"blocks\": [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \":moneybag: High-Value Payment — Paddle (MoR)\"\n }\n },\n {\n \"type\": \"section\",\n \"fields\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Gross Amount:*\\n\" & $currency & \" \" & $total\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Net Earnings:*\\n\" & $currency & \" \" & $earnings\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Customer:*\\n\" & data.customer_id\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Tax (incl.):*\\n\" & $currency & \" \" & data.details.totals.tax\n }\n ]\n },\n {\n \"type\": \"context\",\n \"elements\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"Provider: Paddle (Merchant of Record) | Transaction: \" & data.id\n }\n ]\n }\n ]\n }\n ]\n }\n)"
}
}'Square High-Value Slack Transform:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/transforms \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Square → High-Value Slack Alert",
"type": "jsonata",
"config": {
"expression": "(\n $payment := data.object.payment;\n $amount := $payment.amount_money.amount;\n $currency := $uppercase($payment.amount_money.currency);\n {\n \"attachments\": [\n {\n \"color\": \"#006aff\",\n \"blocks\": [\n {\n \"type\": \"header\",\n \"text\": {\n \"type\": \"plain_text\",\n \"text\": \":moneybag: High-Value Payment — Square\"\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\" & ($payment.customer_id ? $payment.customer_id : \"Walk-in\")\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Location:*\\n\" & $payment.location_id\n },\n {\n \"type\": \"mrkdwn\",\n \"text\": \"*Source:*\\n\" & ($payment.source_type ? $payment.source_type : \"CARD\")\n }\n ]\n },\n {\n \"type\": \"context\",\n \"elements\": [\n {\n \"type\": \"mrkdwn\",\n \"text\": \"Provider: Square | Payment: \" & $payment.id & \" | <https://squareup.com/dashboard/sales/transactions/\" & $payment.id & \"|View in Square>\"\n }\n ]\n }\n ]\n }\n ]\n }\n)"
}
}'Step 5: Test the Pipeline
Send a test payload for each processor to verify routing and transformation.
Test Stripe
curl -X POST https://api.hookbase.app/ingest/your-org/stripe-payments \
-H "Content-Type: application/json" \
-H "Stripe-Signature: t=1234567890,v1=test_signature" \
-d '{
"id": "evt_test_stripe_001",
"type": "checkout.session.completed",
"api_version": "2024-11-20.acacia",
"created": 1709287200,
"data": {
"object": {
"id": "cs_test_stripe_001",
"customer": "cus_test_001",
"subscription": "sub_test_001",
"amount_total": 75000,
"currency": "usd",
"status": "complete",
"payment_method_types": ["card"],
"invoice": "in_test_001",
"total_details": {
"amount_tax": 6000
},
"metadata": {
"plan": "business",
"annual": "true"
}
}
}
}'Test Paddle
curl -X POST https://api.hookbase.app/ingest/your-org/paddle-payments \
-H "Content-Type: application/json" \
-H "Paddle-Signature: ts=1234567890;h1=test_signature" \
-d '{
"event_id": "ntf_test_paddle_001",
"event_type": "transaction.completed",
"occurred_at": "2026-03-01T10:05:00.000000Z",
"data": {
"id": "txn_test_paddle_001",
"customer_id": "ctm_test_001",
"currency_code": "USD",
"details": {
"totals": {
"grand_total": "750.00",
"tax": "120.00",
"fee": "67.50",
"earnings": "562.50"
}
},
"payments": [
{
"method_details": {
"type": "card"
}
}
],
"custom_data": {
"plan": "business",
"source": "website"
}
}
}'Test Square
curl -X POST https://api.hookbase.app/ingest/your-org/square-payments \
-H "Content-Type: application/json" \
-H "X-Square-Hmacsha256-Signature: test_signature" \
-d '{
"event_id": "evt_test_square_001",
"type": "payment.completed",
"data": {
"object": {
"payment": {
"id": "KkAkhdMYtest001",
"customer_id": "VDKXEEKPTEST",
"location_id": "LPNKH7GXTEST",
"amount_money": {
"amount": 82500,
"currency": "USD"
},
"tip_money": {
"amount": 1500
},
"tax_money": {
"amount": 6600
},
"source_type": "CARD",
"status": "COMPLETED",
"created_at": "2026-03-01T10:10:00.000Z"
}
}
}
}'Tip
For production testing, use each provider's test/sandbox mode to generate valid signatures.
Verify Deliveries:
curl https://api.hookbase.app/api/organizations/{orgId}/deliveries?limit=20 \
-H "Authorization: Bearer YOUR_TOKEN"You should see up to 6 deliveries: 3 to Finance API (unified format) and up to 3 to Slack (amounts >= $500 only). Verify that is_merchant_of_record is true only for Paddle transactions.
Production Considerations
1. Enable Deduplication on All Sources
Prevent double-counting when processors retry delivery:
# Stripe — deduplicate on event ID
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/11111111-1111-4111-8111-111111110015 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dedupEnabled": true,
"dedupStrategy": "auto",
"dedupWindowHours": 24
}'# Paddle — deduplicate on notification ID
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/11111111-1111-4111-8111-111111110013 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dedupEnabled": true,
"dedupStrategy": "auto",
"dedupWindowHours": 24
}'# Square — deduplicate on event ID
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/sources/11111111-1111-4111-8111-111111110014 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dedupEnabled": true,
"dedupStrategy": "auto",
"dedupWindowHours": 24
}'2. Set Up Failover for Finance Route
Configure a backup destination to catch failed finance deliveries:
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/destinations \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance API Backup",
"slug": "finance-api-backup",
"type": "http",
"url": "https://finance-backup.yourcompany.com/api/transactions",
"method": "POST",
"headers": {
"Authorization": "Bearer YOUR_BACKUP_FINANCE_KEY",
"Content-Type": "application/json"
}
}'Then enable failover on each finance route (repeat for 44444444-4444-4444-8444-444444440004, 44444444-4444-4444-8444-44444444000d, 44444444-4444-4444-8444-444444440011):
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/routes/44444444-4444-4444-8444-444444440004 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"failoverDestinationIds": ["22222222-2222-4222-8222-22222222001f"],
"failoverAfterAttempts": 3
}'3. Configure Circuit Breaker
Protect downstream services from webhook bursts:
curl -X PATCH https://api.hookbase.app/api/organizations/{orgId}/routes/44444444-4444-4444-8444-444444440004/circuit-config \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"circuitFailureThreshold": 15,
"circuitCooldownSeconds": 300,
"circuitProbeSuccessThreshold": 2
}'4. Set Up Notification Channels
Get alerted when finance deliveries fail:
# 1. Create the channel. Only name, type and config are accepted — the request body is
# validated strictly, so any extra key is a 400.
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/notification-channels \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Finance Pipeline Alerts",
"type": "slack",
"config": {
"webhookUrl": "https://hooks.slack.com/services/YOUR/ALERT/URL"
}
}'
# 2. Link it to each route you want alerts for. The link carries the triggers, and linking
# is how alerts get scoped — a channel on its own notifies about nothing.
curl -X POST https://api.hookbase.app/api/organizations/{orgId}/notification-channels/{channelId}/routes \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"routeId": "{routeId}",
"notifyOnFailure": true,
"notifyOnCircuitOpen": true,
"notifyOnRecovery": true,
"notifyOnSuccess": false
}'The create call returns {"channel": {...}} with the new id; the link call returns
{"success": true}. Repeat step 2 for every route that should alert this channel.
Warning
There is no events array and no filters object. Alerting is configured per
route-channel link, not by subscribing a channel to event names. The four triggers are
notifyOnFailure (default true), notifyOnCircuitOpen (default true),
notifyOnRecovery (default true) and notifyOnSuccess (default false) — that is the
complete set. Slack channels use config.webhookUrl, which must begin with
https://hooks.slack.com/; config.url is the key for type: "webhook" channels only.
Notification channels require a paid plan — on the free plan every endpoint here returns
403.
5. Currency Normalization
If you receive payments in multiple currencies, include original_amount_cents and original_currency fields in your unified schema (as shown in the transforms above). Your finance system can then apply live exchange rates for exact reconciliation. For approximate reporting, you can add a static rate lookup to the JSONata expression using $lookup() against a rates object like {"EUR": 1.08, "GBP": 1.27, "USD": 1}.
Warning
Static exchange rates in JSONata expressions are suitable for approximate reporting only. For exact financial reconciliation, call a live exchange rate API in your finance system.
Related Guides
- Stripe Integration - Stripe-specific setup and signature verification
- Transforms - JSONata syntax and advanced examples
- Filters - Complex routing logic and conditions
- Deduplication - Prevent duplicate event processing
- Failover - Ensure critical events are delivered
- Circuit Breaker - Protect downstream services
- Notification Channels - Configure delivery alerts
See Also
- Stripe Integration — Stripe webhook setup
- Paddle Integration — Paddle webhook setup
- Square Integration — Square webhook setup
- Stripe Payment Fan-Out — Related use case