Architecture
Stripe webhooks can’t be pointed directly at Scrip. The payload formats, authentication, and data conventions are different, so you need a thin middleware layer between them: The middleware is small, about 60 lines of code. It verifies the Stripe signature, extracts the fields Scrip needs, and forwards the event. No business logic lives here; all reward calculations happen in Scrip rules. What you need:| Stripe side | Scrip side |
|---|---|
| A Stripe Issuing integration with active cards | A program created for your card rewards |
| A webhook endpoint (configured in Webhook setup) | An asset linked to that program (e.g., POINTS or CASHBACK_USD) |
Participants enrolled with external_id set to the Stripe cardholder ID (e.g., ich_...) | |
| An API key for your middleware to call the Scrip API |
cardholder ID as the Scrip external_id. This means each Scrip participant’s external_id must match their Stripe cardholder ID:
metadata field is a good place to store your internal user ID.
Webhook setup
In the Stripe Dashboard or via the API, create a webhook endpoint that points to your middleware URL and subscribes toissuing_transaction.created:
whsec_...). You’ll use it to verify incoming requests.
We recommend calculating rewards on settled transactions only. You only need
issuing_transaction.created. This single event covers both captures (settlement) and refunds. The transaction’s type field distinguishes them. Authorizations (issuing_authorization.*) are not settlement and can be reversed, expired, or never captured, which makes them unreliable for reward calculations. See Stripe’s transaction lifecycle for details.Middleware
You need to build a small service that sits between Stripe and Scrip. It receives Stripe webhook events, verifies the signature, transforms the payload into the format Scrip expects, and forwards it toPOST /v1/events. The examples below are complete and ready to deploy.
| Stripe field | Scrip field | Transformation |
|---|---|---|
data.object.id | idempotency_key | Use directly (e.g., ipi_1Mz...) |
data.object.cardholder | external_id | Use directly (e.g., ich_1Mz...) |
data.object.created | event_timestamp | Convert Unix timestamp to RFC 3339 |
data.object.type | event_data.type | Map "capture" → "purchase", "refund" → "refund" |
data.object.amount | event_data.amount | abs(amount) / 100. Stripe uses cents, negative for captures. |
data.object.currency | event_data.currency | Uppercase (e.g., "usd" → "USD") |
data.object.merchant_data.category_code | event_data.mcc | Use directly (e.g., "5812") |
data.object.merchant_data.name | event_data.merchant_name | Use directly |
data.object.merchant_data.city | event_data.merchant_city | Use directly |
data.object.merchant_data.country | event_data.merchant_country | Use directly |
data.object.authorization | event_data.authorization_id | Use directly. Links refunds to the original purchase. |
data.object.wallet | event_data.wallet | "apple_pay", "google_pay", "samsung_pay", or null |
event_data that are useful for your program. Stripe’s Transaction object includes purchase details, network data, and more. Anything you add to event_data is available in rule conditions as event.<field_name>.
Rules
With the middleware in place, Stripe transactions arrive as Scrip events with a consistentevent_data shape. Here’s a complete rule set for a cashback card with category multipliers and refund handling.
Earning rules
Award cashback when event_data.type is "purchase":
stop_after_match prevents the base rule from stacking. See Category multipliers for a deeper walkthrough.
Reversal rules
When a cardholder receives a refund, the cashback they earned on that purchase should be clawed back. Reversal rules mirror the earning rules with DEBIT instead of CREDIT, using the same multiplier per MCC so the debit matches what was originally awarded:
The refund rules above use
"allow_negative": true so the debit succeeds even if the cardholder has already spent their cashback. The balance goes negative and is offset by future earnings. Without allow_negative, a DEBIT fails when the available balance is insufficient, and the refund event would be marked FAILED. See Negative Balances for more on this behavior.| MCC | Category |
|---|---|
| 6010, 6011, 6012 | Cash advances / ATM withdrawals |
| 6051 | Quasi-cash (money orders, wire transfers) |
| 4829 | Wire transfers |
| 7995 | Gambling |
stop_after_match rule at a low order number that matches excluded MCCs and takes no actions. This swallows the event before it reaches earning rules.
Transaction lifecycle
Authorizations are not final. They can be reversed, expired, partially captured, or over-captured. Awarding points on authorization would require reversing them on every one of these outcomes. Waiting for thecapture transaction avoids this complexity entirely.
The Stripe Transaction object includes an authorization field that references the original authorization ID. This link is present on both captures and refunds.
| Scenario | authorization field |
|---|---|
| Normal capture | Set to the authorization ID |
| Normal refund | Usually set to the original authorization ID |
| Force capture (no prior auth) | null |
| Unlinked refund | null |
authorization_id through in event_data. You can reference it in rule conditions if needed. For example, to skip refund processing on force captures:
Stripe says linking refunds to authorizations is “an inexact science”. Some refunds arrive with
authorization: null. For category-based reward rates, this doesn’t matter. The refund carries its own MCC and the correct debit rate can be calculated independently. For more complex scenarios (threshold-based rates, tiered earn rates), see Edge cases.Edge cases
Partial refunds. Stripe refunds can be for less than the original capture amount. The refund transaction’samount reflects only the refunded portion, and your reversal rules calculate the debit from that amount. No special handling needed. A $40 refund on a $100 dining purchase debits $2.00 (5% of $40).
Multi-capture transactions. Airlines, hotels, and car rental companies can create multiple capture transactions against a single authorization. Each capture arrives as a separate issuing_transaction.created event with a unique transaction.id, so they map to separate Scrip events with distinct idempotency keys.
Force captures. Some merchants settle without a prior authorization (e.g., offline transactions). These arrive as type: "capture" with authorization: null. Award points normally. The transaction is still a valid settled purchase.
Refund reversals. Stripe can reverse a refund if it was issued in error. This appears as a transaction with type: "refund" and a negative amount. Your middleware should detect this case and map it to a "purchase" type instead:
event.original_earn_rate and use a priority refund rule that references it:
original_earn_rate is present, and the standard MCC-based refund rules handle cases where it isn’t.
Disputes. Stripe Issuing disputes follow a separate lifecycle. If a cardholder disputes a transaction and wins, the funds return to the issuing balance but no issuing_transaction.created event fires. Instead, Stripe emits issuing_dispute.* events. If you need to reverse points on successful disputes, subscribe to issuing_dispute.closed in your Stripe webhook and forward it as a refund event when dispute.status === "won". For many programs, dispute-based reversals aren’t necessary. The volume is low and the complexity isn’t worth it.
Testing
Stripe Issuing supports test mode. Create test cardholders and simulate transactions:issuing_transaction.created webhook event in test mode, which flows through your middleware to Scrip.
You can also test rules independently of Stripe by sending events directly to Scrip:
Example
A cardholder buys an $85 lunch at a restaurant (MCC 5812).Cardholder pays at a restaurant
The card is charged $85. Stripe creates an authorization and holds the funds on the issuing balance. No event is sent to Scrip yet.
Merchant settles the transaction
The next day, the merchant captures the authorization. Stripe creates a Transaction object and fires
issuing_transaction.created:Middleware transforms and forwards
Your server receives the webhook, verifies the Stripe signature, and maps the payload to a Scrip event. The amount converts from cents to dollars (
-8500 → 85.00), the type maps from "capture" to "purchase", and the cardholder ID becomes the external_id:Scrip processes the event
dining_5pct rule matches (event.type == "purchase" and MCC 5812). Credits $4.25 (5% of $85). stop_after_match prevents the base rule from also firing. Event status transitions to COMPLETED.Next steps
This guide covers the core integration. To build on it:- Add spend thresholds, retroactive bonuses, and monthly resets with the Cashback Card example
- Browse more rule recipes like sign-up bonuses, streaks, and tiered earn rates in Common Patterns
- Learn how events flow through the processing pipeline in Event Processing