# Stripe and webhook catchers

> Connect Stripe, or catch any platform's webhook, map its fields to Kalman events, test the mapping, and watch for changes.

Updated 2026-11-10 · Sources · https://kalmananalytics.com/support/webhook-catchers/

Kalman can take in events from platforms that send webhooks. Stripe has its own built-in connection. For anything else, use a **webhook catcher**: a Kalman URL that accepts a platform's payload as it already sends it.

## Connect Stripe

1. Open **Integrations**. Under **Payments**, find **Stripe** and click **Connect**.
2. Copy the **Webhook URL**.
3. In Stripe, go to Developers → Webhooks, add the URL, and subscribe to these events:
    - `checkout.session.completed` and `payment_intent.succeeded` become `purchase`.
    - `invoice.paid` becomes `refill_purchased`, but only for subscription renewals.
    - `charge.refunded` becomes `refund_issued`.
    - `customer.subscription.deleted` becomes `subscription_canceled`.
4. Paste the endpoint's signing secret (it starts with `whsec_`) into **Stripe signing secret** and click **Save**. Kalman rejects deliveries until a secret is saved. After saving, the secret can't be viewed again.

### Where to put `kl_id` and the product SKU

Kalman reads `kl_id` (the visitor's `_kl_id` cookie) and `kalman_product_sku` only from the **metadata of the object in each event**. It doesn't look them up on related objects. Set them on every object whose events you send:

| Stripe event                    | Object Kalman reads | Where to set `kl_id` and `kalman_product_sku`                                                                                                                       |
| ------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checkout.session.completed`    | Checkout Session    | `client_reference_id` (for `kl_id`) or the session's `metadata`                                                                                                     |
| `payment_intent.succeeded`      | PaymentIntent       | `payment_intent_data.metadata` when you create the Checkout Session, or the PaymentIntent's `metadata`                                                              |
| `invoice.paid` (renewals)       | Invoice             | the invoice's `metadata`. Stripe doesn't copy subscription metadata onto renewal invoices, so set it on each invoice, for example from an `invoice.created` handler |
| `charge.refunded`               | Charge              | the charge's `metadata`. Check in Stripe that it carries `kl_id`; if it doesn't, the refund isn't joined to the visit                                               |
| `customer.subscription.deleted` | Subscription        | `subscription_data.metadata` at checkout, or the subscription's `metadata`                                                                                          |

To match products without a SKU, add Stripe price IDs to a product ([Products and aliases](/support/products-and-aliases/)). Kalman finds the price on invoice lines and subscription items, or in `metadata.price_id`. A Checkout Session event doesn't include its line items unless your endpoint expands them, so use `kalman_product_sku` there.

### Format, test mode and duplicates

- **Payload:** Stripe's standard event JSON (`{"id": "evt_…", "type": …, "data": {"object": …}}`), verified with the `Stripe-Signature` header and your `whsec_` secret. Deliveries more than 5 minutes old are rejected.
- **API version:** Kalman doesn't pin one. It reads both the older and the newer invoice-line shapes. If you upgrade your Stripe API version, send a test event and check the Events log.
- **Test and live:** each Kalman Stripe source has one signing secret, and Kalman doesn't check Stripe's `livemode` flag. Use a **separate** Kalman Stripe source, with its own URL and secret, for your test-mode endpoint, so test purchases never mix with real ones.
- **Duplicates:** a purchase, refill, refund or cancellation is counted once per order, so a retried or duplicated delivery doesn't double-count. Other events are de-duplicated by Stripe's event id.
- **Order:** deliveries are recorded at the time Stripe says the event happened. Kalman doesn't reorder them: a refund that arrives before its purchase is still recorded as a refund.

## Create a webhook catcher

Click **Connect** on **Webhook catcher** under **Custom**, or click **Connect with a webhook catcher** on any platform card. A bar at the top of the catcher's page tracks the steps: Choose platform, Copy URL and token, Send test events, Map fields, Event types, Test mapping, Go live.

The **Endpoint** panel gives two URLs:

- **Webhook URL (includes token)**: paste this into the sending platform.
- **Webhook URL (token in header)**: use this if the platform can set headers. Send the token as `Authorization: Bearer <token>` or in `X-Kalman-Token`. This keeps the token out of server logs.

If the sender can sign requests the way Kalman does, change **How the sender proves itself** to **Kalman HMAC signature**.

## Catch a sample

1. A new catcher listens for 15 minutes. After that, click **Listen for 15 minutes** to listen again.
2. In the sending platform, trigger each event you care about, such as a test order or a workflow run on a test contact.
3. Click **Refresh**. Each delivery is kept as an encrypted sample. Samples are not recorded as events yet.

## Map fields

1. **Fields seen** lists each field path, its kind and how often it appears. Kalman never shows the values.
2. Choose a Kalman field in each row's dropdown (email, phone, `order_ref`, `value`, `product_sku`, `kl_id` and so on).
3. If one payload holds several events, set **Events array path**. Tick **Values are in cents** if the amounts are in cents.
4. Click **Save**.

## Name the events

- **From a field:** map a field to `event`. **Event types seen** lists its values, and you translate each one, for example `order.paid` → `purchase`. Kalman doesn't record a name that looks sensitive.
- **From the URL:** add `&event=purchase` (or `lead`, `provider_approved`…) to the end of a workflow's URL. If a mapped event field is present, it takes priority.

## Test, then go live

1. Click **Test mapping** on a sample. Kalman shows what it would record. Identity fields show only as hashed, and nothing is recorded.
2. Click **Process samples** to record the caught samples. Mapped events then go to your connected ad platforms.
3. A new catcher **records only** until you start it: deliveries are kept as samples, even after listening ends, and nothing becomes an event. When the mapping is right, click **Start processing**. From then on, new deliveries become events. Samples already caught wait for **Process samples**. **Switch to recording only** goes back.

Catchers created before November 9, 2026 keep processing as they did.

## Monitor

- **Recent deliveries** shows the time, status and detail for each delivery. It never shows payloads.
- If a mapped field goes missing from deliveries, a warning appears: "The sender's payload may have changed." Catch a fresh sample and check the mapping. Saving the mapping resets the warning.

Raw bodies are stored encrypted and deleted after 30 days. Emails and phones are hashed on arrival.

**What Kalman never sends:** ad platforms get only allowlisted fields. They never receive product names or SKUs, raw payload fields, or plain-text contact details.

Still stuck? Log in and open a request: https://app.kalmananalytics.com/support
