# Connect your billing system

Twelve billing systems connected directly, the live setup status, and test data that never touches real money.

A referral program needs to know three things: who arrived through a partner's
link, who signed up, and who paid (or asked for their money back). The link and
the tag on your site answer the first. Your **billing system** answers the last,
and Bolt Claw connects to twelve of them directly, so most programs need no code
on the server at all.

Connect yours from the dashboard: open the program, **Setup**, **Billing**, pick
the system you take payments with. Each card says exactly what to paste where.
Connecting one is done in the dashboard and never through the API or an AI
assistant, because every integration needs a credential pasted or shown once,
and a secret must never pass through a chat with a model.

Then watch the **setup status** at the top of the same tab: one row per part, each
`Connected`, `Connected with warnings` or `Not connected`, with the exact fix.
When it is all green, press **Send a test sale** to see the whole path work
without real money.

## Every billing system, at a glance

| Billing system | How a delivery proves itself | Codes typed at checkout | Live or test | Disputes | Partial refunds | Guide |
|---|---|---|---|---|---|---|
| Stripe | Signing secret, or the Bolt Claw Stripe App | Learned from promotion code events | Each event says | Yes | Yes | [Stripe](https://boltclaw.ai/docs/referral-billing-stripe) |
| Paddle Billing | Signing secret, timestamped | Learned from discount events | Sandbox switch | Yes | Yes | [Paddle](https://boltclaw.ai/docs/referral-billing-paddle) |
| Polar | Standard Webhooks | Arrive as text | Sandbox switch | No | Yes | [Polar](https://boltclaw.ai/docs/referral-billing-polar) |
| Whop | Standard Webhooks | Arrive as text | Sandbox switch | Yes | Yes | [Whop](https://boltclaw.ai/docs/referral-billing-whop) |
| RevenueCat | A header token Bolt Claw makes, plus an optional signature | Offer codes arrive as text | Each event says | No | No | [RevenueCat](https://boltclaw.ai/docs/referral-billing-revenuecat) |
| WooCommerce | Signing secret, no timestamp | Arrive as text | Sandbox switch | No | Yes | [WooCommerce](https://boltclaw.ai/docs/referral-billing-woocommerce) |
| FastSpring | Signing secret, no timestamp | Arrive as text | Each event says | Yes | Yes | [FastSpring](https://boltclaw.ai/docs/referral-billing-fastspring) |
| Lemon Squeezy | A signing secret Bolt Claw makes | Not sent at all | Each event says | No | Yes | [Lemon Squeezy](https://boltclaw.ai/docs/referral-billing-lemonsqueezy) |
| Shopify | The store's signing key, no timestamp | Arrive as text | Each order says | Yes | Yes | [Shopify](https://boltclaw.ai/docs/referral-billing-shopify) |
| Chargebee | Basic auth Bolt Claw makes (no body signature) | Not read | Sandbox switch | No | Yes | [Chargebee](https://boltclaw.ai/docs/referral-billing-chargebee) |
| Recurly | Signing secret, plus a read-only key | Not read | Sandbox switch | No | Yes | [Recurly](https://boltclaw.ai/docs/referral-billing-recurly) |
| Gumroad | None: every sale is re-read from Gumroad | Offer codes arrive as text | Each sale says | Yes | No | [Gumroad](https://boltclaw.ai/docs/referral-billing-gumroad) |

Every integration has its own endpoint URL, shown on its card, and it answers
nothing until it is set up. What a delivery gets back:

| Status | When |
|---|---|
| `200` | Every delivery that verified, whether it counted or not (a refund of a sale this program never saw, a status Bolt Claw does not count). Sending it again will not change the answer. |
| `400` | A bad or stale signature, or a body that cannot be read. |
| `401` | The wrong credentials, for the two systems where Bolt Claw made them (Chargebee, RevenueCat). |
| `404` | No such integration, or one that is switched off or not set up yet. |
| `413` | A body over 1 MiB. |
| `500` | Our fault, or the billing system's own API was unreachable: it retries, and the retry is read. |

**Nothing counts twice.** A billing system that signs no timestamp (WooCommerce,
FastSpring, Lemon Squeezy, Shopify, Chargebee, Gumroad) could have a delivery
replayed, so each delivery id is claimed before the body is read, and each sale,
refund and dispute has a stable id of its own: a replay, a retry and a
redelivery all write nothing new. Each billing system is its own source, so its
event ids never collide with yours or with another system's.

**The customer.** Every system can carry your own id for the customer (the
field `boltclaw_customer_id`, renamable on the card), so a sale is matched to a
signup you reported. Without it, the billing system's own customer id stands
in, and its renewals still find the customer.

## Not on any of these

Report sales from your own server with [`POST /v1/referrals/conversions`](https://boltclaw.ai/docs/api/referrals):
a key with the **Referral tracking** preset (`conversions:write` alone), your own
`event_id` for each sale, signup, refund and dispute, and the code the tag kept.
The setup status counts a sale that arrived this way as a connected billing
system.

## Stripe: one click, or a webhook

**The Bolt Claw Stripe App** connects Stripe with no webhook to create and no
secret to paste: **Connect Stripe** sends you to Stripe, you approve the app on
your account, and you come back connected. It asks only for what it uses:
reading events, Checkout Sessions, invoices, charges and promotion codes, and
writing coupons, promotion codes, customers and subscriptions (for the friend's
discount codes, account credit and free months). It holds no payout permission.
One Stripe account can be connected to one program at a time. Uninstalling the
app in Stripe disconnects it here; disconnecting it here uninstalls it in
Stripe too. Every conversion it recorded stays.

**The webhook** is the way that needs no app: add an endpoint in Stripe with the
URL on the card, select the events it lists, and paste the endpoint's signing
secret. Two things matter on Stripe's side:

- **Choose Snapshot, not Thin.** Stripe's dashboard now defaults new endpoints to
  Thin events, which carry only an id. Bolt Claw records that it received one,
  and the setup status says "Choose Snapshot".
- **Select `invoice_payment.paid`.** On Stripe API versions from `2025-03-31`,
  an invoice no longer names the payment that paid it, so a refund of a
  subscription invoice could not find its sale. `invoice_payment.paid` links
  the two, with no key at all, and a refund is then matched whichever of the
  events arrives first. The setup status warns when it is missing.

Either way, a Stripe event says whether it is live or a test, and a test-mode
event is recorded as [test data](#test-data).

## Stripe Payment Links, with no code

If you sell through Stripe Payment Links or the Buy Button, the website tag is
the whole integration:

```html
<script src="https://boltclaw.ai/referrals/v2.js" defer data-payment-links="auto"></script>
```

It puts the partner's code on every Payment Link on the page as
`client_reference_id` (and the friend's discount code as
`prefilled_promo_code`, when the program gives the friend one), and on every
`<stripe-buy-button>` as `client-reference-id`. A value you already set is left
alone. Connect Stripe as above and paid checkouts are credited by that id.

## The live setup status

At the top of the Setup tab, and at
[`GET /v1/referrals/programs/{id}/integration-status`](https://boltclaw.ai/docs/api/referrals) for the
CLI and an AI assistant. Every row is decided from what Bolt Claw has actually
seen for this program: live data only, because a test sale proves the path and
not your own wiring.

| Part | Connected when | Warnings |
|---|---|---|
| Website tag | The tag was seen on your product page | You have version 1 rather than 2; your audience includes the EU or the UK and the tag does not wait for consent |
| Billing | A verified event in the last 30 days, or a sale through the API | Deliveries refused since the last good one (usually the wrong secret); Stripe's Thin payloads; an old Stripe API version; `invoice_payment.paid` not selected |
| Signups | A signup (`lead`) arrived | Signups named by an email address rather than your own id |
| Sales | A sale arrived | Sales in the last 7 days, none of them referred, while clicks arrive |
| Codes | A sale was credited by a discount code alone, or your billing system's code events arrived | Your billing system's codes match none of this program's codes |
| In-app panel | Your server minted a panel session and a panel was opened | No site is allowed to show the panel |
| Friend's welcome | The friend's offer was shown to a visitor | |

A part your own setup does not use is marked as skipped and does not count:
signups when people only sign up at checkout, the panel while it is switched
off, codes while the program has none.

## Seeing the website tag

**Check again** fetches your product page once, from our server, and looks for
`/referrals/v2.js` (or `v1.js`) in a script tag. The fetch is careful about
where it goes: `https` only, only your program's own hosts (the product URL's,
and the ones you allowed the welcome to read from), never a private address
(checked after the name is resolved, and again on every redirect, three at
most), five seconds and 512 KB at most, and a few checks per program an hour.
[`POST /v1/referrals/programs/{id}/integration-status/tag-check`](https://boltclaw.ai/docs/api/referrals)
does the same from the API.

A tag your site adds with JavaScript (a tag manager, a single-page app) is
invisible to a server-side fetch. For those, **Open your site with this link**
opens your product with `?boltclaw_verify=<nonce>`: the tag sees that parameter
and makes one request to say it is there, carrying the nonce, its version and
whether consent mode is on, and nothing else. The link works once, for half an
hour. Without that parameter in the address, the tag never makes this request.

## Test data

Test data is the real path with nothing real at the end of it. It comes from
four places:

- **Send a test sale** on the Setup tab, or
  [`POST /v1/referrals/programs/{id}/test-sale`](https://boltclaw.ai/docs/api/referrals): a click on
  the program's test partner's link, a signup, a sale of 100.00 in the program's
  currency with two items, then a partial refund of 25.00. Each step shows what
  it caused as it happens: the commission and why, any reward that is not cash,
  the friend's offer, and the webhook deliveries it queued.
- **`test: true`** on [`POST /v1/referrals/conversions`](https://boltclaw.ai/docs/api/referrals).
- **A test-mode event** from a billing system whose events say so (Stripe,
  FastSpring, Lemon Squeezy, RevenueCat, Shopify, Gumroad).
- **The Sandbox switch** on an integration's card, for a sandbox account whose
  events do not say (Paddle, Polar, Whop, WooCommerce, Chargebee, Recurly): add
  a second integration for the sandbox and switch it on. Every event it receives
  is test data.

What test data never does:

- **It is never paid.** A test commission never reaches a payout, and the
  database refuses one in a payout or marked paid.
- **It is never counted.** Not in the program's stats, not in the click
  allowance, not in any total.
- **It is credited to "Test partner" only**, the program's own test partner,
  whoever's link or code it carried. No partner ever sees your tests. The test
  partner's link is archived from the start, so no visitor can open it.
- **It emails nobody.**
- **It reaches a webhook only if the endpoint asked.** Switch **Also send test
  events** on for an endpoint (`test_events` on
  [`POST /v1/webhooks`](https://boltclaw.ai/docs/webhooks)); a test delivery's body carries
  `"test": true`, and a live one never does.
- **It never collides with live data.** A test `event_id` and a live one with
  the same text are two different conversions, and a test customer and a live
  one with the same id are two different customers.

Conversions, commissions and rewards from test data carry `test: true`, and the
dashboard marks them **Test**. **Clear test data** (or
[`DELETE /v1/referrals/programs/{id}/test-data`](https://boltclaw.ai/docs/api/referrals)) deletes
every test row of the program and its test partner, and touches nothing live.
An AI assistant is always asked first.

## Where a setup is weaker, and what to do

- **Chargebee** signs nothing: it can only protect its webhook with Basic auth.
  Bolt Claw makes a random username and password, keeps only the password's
  hash, and lets you make new ones at any time. Keep them private, and make new
  ones if they ever leak.
- **Gumroad's ping** is not signed, so nothing in it is ever counted: every sale
  it names is read from Gumroad with your access token, and only what Gumroad's
  API returns is recorded. A partial refund cannot be told from a full one in
  Gumroad's data, so report a partial refund through the API.
- **Lemon Squeezy** sends no discount code, so a sale made with a code alone
  cannot be credited: use links (or pass the referral at checkout).
- **Recurly** sends ids only, so each invoice is read through Recurly's API with
  a read-only key before it counts.
- **RevenueCat**'s header token alone proves who sent a request but not what it
  said; turn on RevenueCat's signing and paste its secret too.

## Quickstarts

By billing system: [Stripe](https://boltclaw.ai/docs/referral-billing-stripe),
[Paddle](https://boltclaw.ai/docs/referral-billing-paddle), [Polar](https://boltclaw.ai/docs/referral-billing-polar),
[Whop](https://boltclaw.ai/docs/referral-billing-whop), [RevenueCat](https://boltclaw.ai/docs/referral-billing-revenuecat),
[WooCommerce](https://boltclaw.ai/docs/referral-billing-woocommerce),
[FastSpring](https://boltclaw.ai/docs/referral-billing-fastspring),
[Lemon Squeezy](https://boltclaw.ai/docs/referral-billing-lemonsqueezy),
[Shopify](https://boltclaw.ai/docs/referral-billing-shopify), [Chargebee](https://boltclaw.ai/docs/referral-billing-chargebee),
[Recurly](https://boltclaw.ai/docs/referral-billing-recurly) and [Gumroad](https://boltclaw.ai/docs/referral-billing-gumroad).

By how your product is built: [Next.js](https://boltclaw.ai/docs/referral-quickstart-nextjs),
[Express](https://boltclaw.ai/docs/referral-quickstart-express),
[Supabase](https://boltclaw.ai/docs/referral-quickstart-supabase),
[Firebase](https://boltclaw.ai/docs/referral-quickstart-firebase), [Clerk](https://boltclaw.ai/docs/referral-quickstart-clerk),
[Auth.js](https://boltclaw.ai/docs/referral-quickstart-authjs),
[Better Auth](https://boltclaw.ai/docs/referral-quickstart-better-auth),
[Django](https://boltclaw.ai/docs/referral-quickstart-django), [Rails](https://boltclaw.ai/docs/referral-quickstart-rails),
[Laravel](https://boltclaw.ai/docs/referral-quickstart-laravel) and
[a site with no code](https://boltclaw.ai/docs/referral-quickstart-nocode).

Moving from another tool: [Moving from Rewardful, FirstPromoter, Tolt or Dub](https://boltclaw.ai/docs/referral-migrate).

Working with an AI coding assistant: the dashboard's **Copy a prompt for your AI
coding assistant** hands it your program's id, your stack and the steps, and
points it at `https://boltclaw.ai/docs/referrals/install.md`, a page written for
coding agents. It never contains your key.

<!-- https://boltclaw.ai/docs/referral-setup as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
