Skip to main content

Verify your Dub + Stripe referral tracking before launch: one test customer, four records

Trace one test customer from a partner's link to signup, Stripe payment and refund on Dub, with the IDs to record, what a pass looks like, and what the test costs.

Updated 13 min readMarkdown

Maya clicks your partner Acme's link on a Tuesday, signs up with Google, pays for your plan through Stripe Checkout, and asks for her money back nine days later. That one customer leaves four records behind: a click, a lead, a sale and a refund. If any of them is missing, or lands on the wrong partner, you'll hear about it from Acme, probably in an email with a screenshot attached.

So before a real partner sends you traffic, verify your referral tracking end to end: push one test customer through all four on Dub and Stripe, and write down the IDs at each hop.

I'm a Dub affiliate. I checked every step against Dub's docs on Nov 11, 2026, but haven't run it in a live or staging workspace myself. Maya, Acme and every ID below are made up. The "Observed" and "Pass/Fail" columns in the table are blank on purpose, for your own run.

What you need first

This is for a SaaS running its own partner program on Dub with Stripe billing, and it covers one path: Stripe Checkout in subscription mode. Other payment setups are listed at the end. Run them as separate traces.

  • A Dub staging workspace. Staging is on Business plans and above, and only when the workspace has a partner program. Free and Pro workspaces don't get one (testing docs). Business is $90 a month, or $81 a month billed yearly (pricing). Staging itself isn't billed (testing docs). Dub's 14-day trial charges your card for the first month when it ends, and you can cancel any time before that (free trial). The docs don't say whether a workspace still on its trial gets staging, so check first.
  • Staging keys, not production ones. Staging API keys start with dub_test_ and staging publishable keys with dub_pk_test_. API keys, publishable keys, webhooks and integration connections are all separate from production (same page).
  • Stripe in test mode or a Stripe sandbox, connected to the staging workspace. Live mode is rejected in staging.
  • The Dub Analytics script on your site and Dub's server-side SDK in your backend (Stripe integration).
  • Test partner email addresses. Staging payouts are simulated, but "Partners still receive the payout email for staging payouts" (testing docs).

The cast

  • Acme is a test partner in your staging program, with a staging partner link.
  • Maya is a test user. She clicks Acme's link, signs up, subscribes, then asks for a refund.
  • Your app is on app.example.com and your marketing site on example.com.

Write down every ID as you go. The trace is only as good as what you record.

The ID chain

Fill this in before you click anything. Each row has to hand off to the next.

Hop Where it lives Example value Has to match
Click ID dub_id cookie set by the Analytics script clk_EXAMPLE Sent as clickId in the lead event
Your user ID Your database user_maya_001 Sent as customerExternalId in the lead event
Checkout link-back metadata.dubCustomerExternalId on the Checkout Session user_maya_001 Exactly the customerExternalId above
Stripe customer cus_... cus_EXAMPLE Created by Checkout for Maya
Subscription and invoice sub_..., in_... sub_EXAMPLE, in_EXAMPLE The paid invoice behind the sale
Charge ch_... ch_EXAMPLE The charge you refund

The rule that's easiest to break: the value you send as customerExternalId at signup has to be the same value you put in the Checkout Session's metadata.dubCustomerExternalId. If one is your user ID and the other is an email or a team ID, the sale can't tie back to the click (Stripe integration).

The trace, step by step

1. Check the script, and that dub_id reaches signup. On a page where the Analytics script is installed, open the browser console and type _dubAnalytics. You should see the object. Then load that page with ?dub_id=test and check that a dub_id cookie appears. If either check fails, Dub's docs say to make sure the script is in the page's <head> and to purge any CDN cache (install checks). On a partner program, also set the script's data-domains property to your referral link domain; the same page says you need it "to enable client-side click-tracking." The check that matters is whether the dub_id cookie reaches your signup handler.

Pass: _dubAnalytics is defined and the ?dub_id=test cookie appears, with data-domains set.

2. Click Acme's staging link as Maya, in a fresh browser profile. Staging links are on {your-workspace-slug}.staging.dub.link (testing docs), so write that domain down along with the dub_id value and the domain the cookie is set on.

The cookie lasts 90 days by default, and Dub gives the sale to the last partner link clicked unless you switch to first-click (how attribution works).

Pass: the click shows up in the staging workspace, and the dub_id cookie can be read on your signup domain, so the handler can send it as clickId.

3. Sign Maya up. Your backend sends a lead event with clickId from the cookie, an event name like Sign Up, and customerExternalId set to Maya's user ID. Dub calls server-side tracking "the recommended way to track conversions" (server-side tracking).

If sign-in runs through a callback route, like OAuth or a magic link, send the lead inside that callback, where the dub_id cookie is still available. Dub's Supabase example tracks the lead only when the cookie is there and the user was created in the last 10 minutes, and then deletes the cookie (Supabase guide). You don't need Supabase to copy the idea.

Dub records only the first lead for each customerExternalId and eventName pair (how attribution works). So a retried callback shouldn't double-count, as long as every signup path sends the same event name and the same user ID. Test it: fire the callback twice and confirm one lead.

If a signup alone isn't the lead you want to pay for, say a partner should only earn once a user does something real in your product, Dub has deferred lead tracking, which creates the lead event in a later request (deferred lead tracking). Trace that as its own run.

Pass: one lead for Maya in staging, on Acme's link, with customer external ID user_maya_001.

4. Start a subscription Checkout. Create the Checkout Session with mode: "subscription" and metadata.dubCustomerExternalId set to Maya's user ID. Pay with one of Stripe's test cards (Stripe testing docs).

Pass: a sale on Maya's customer in staging, on Acme's link, with a commission for Acme.

5. Check the amount. Dub leaves tax out of the sale amount. For a Checkout Session it records amount_total - total_details.amount_tax. For invoices it records amount_paid × total_excluding_tax / total, so "A fully paid invoice records exactly total_excluding_tax," and a partly paid one, say with a credit balance applied, records the paid amount without its tax share (same page). If an invoice has several line items, Dub works out the commission line by line. Compare Dub's sale amount to that number and to the reward you set.

Say Maya's plan is $49 a month and 8% sales tax brings the invoice to $52.92. She pays it in full, so either way Dub should record $49.00: $52.92 minus $3.92 of tax on the session, or $52.92 × $49.00 / $52.92 on the invoice. At a 20% reward, Acme's commission is $9.80.

Pass: sale amount $49.00, commission $9.80.

6. Refund the charge in full, before the commission is paid out, and to be safe before its payout starts processing. With the Stripe integration, "refunds are automatically tracked without any additional setup" (refunds docs), and Dub's payouts help says the commission "will also be marked as refunded" (payouts help).

Pass: Acme's commission shows refunded. Then check whether it's still in the pending payout. The docs say a refunded commission is left out of pending payouts on the manual path, but don't spell that out for Stripe refunds, so record what you see.

Dub's docs don't say how a partial refund changes the commission. If you offer partial refunds, run one as its own row and record what happens.

7. Test a refund after the payout. Staging payouts are simulated, with no bank charge and no money moving (testing docs), so you can confirm one, refund the charge after it, and record whether the commission changes. If your program uses external or hybrid payouts, staging sends external_payout.confirmed, and the payout stays processing until a test handler responds, so point that webhook at one first (same page). Dub's refunds docs say "Commissions that are already paid cannot be refunded." They suggest a payout holding period that matches your refund policy, so a 30-day refund window gets a 30-day hold, and a clawback for anything already paid. Pick your holding period and write the clawback steps down now.

8. Go live on purpose. Swap every dub_test_ and dub_pk_test_ key for production keys, connect live Stripe to the production workspace, and send one real conversion through a production link. Dub's docs say to confirm it lands in production "before you treat the integration as live." Staging data stays in staging (testing docs).

The test table

Copy this, run each row in staging and fill in the blank columns.

Scenario Expected Observed IDs to record Pass/Fail Fix if it fails
Email signup One lead on Acme's link, customerExternalId = your user ID dub_id, user ID, lead event Lead not sent, or sent without the cookie value. Check the signup handler reads dub_id.
OAuth or callback signup One lead, even if the callback runs twice dub_id, user ID, event name, callback time vs. user created time Cookie lost on the provider redirect, or a new-user check filtered the wrong person. A second lead means a different eventName or customerExternalId on this path.
Marketing site to app subdomain dub_id set on example.com is readable on app.example.com Cookie domain on each host If the script is only on the subdomain, set the cookie domain to .example.com (cross-domain)
Two different domains Click ID carried from example.sh to example.com URLs before and after the hop Use the outbound-domains script on both domains (same page)
Paid subscription Sale on Maya's customer, on Acme's link, commission created cus_, sub_, in_, Checkout Session ID dubCustomerExternalId missing, or not equal to the lead's customerExternalId
Full refund before payout Commission marked refunded ch_, refund ID, commission ID Stripe app not connected to the staging workspace, wrong charge, or the commission was already paid (or its payout was processing)
Refund after a staging payout Per the refunds docs, no automatic change. You handle it with a clawback Payout ID, commission ID Set a holding period to match your refund window and write down the clawback steps
Staging vs. production Staging events stay in staging. Production uses dub_ and dub_pk_ keys and live Stripe Key prefixes in each environment A dub_test_ key shipped to production. Replace it and redeploy.

When something's off

  • Clicks show up, leads don't. The lead event isn't firing, or it has no click ID. Check that the cookie exists at signup time on the signup domain, and that every signup method runs the lead call on the server.
  • Leads show up, sales don't. Check the Checkout metadata key is dubCustomerExternalId and that its value equals the lead's customerExternalId. Then check the Stripe app is connected to the same workspace, staging or production (Stripe integration).
  • A one-time purchase didn't track. In guest checkout with mode: "payment", the Checkout customer can be null and "sales won't be tracked on Dub." Dub's fix is customer_creation: "always" (same page). It's off the subscription path, and it still catches people.
  • The sale amount looks low. Tax is left out on purpose, as in step 5's $52.92 invoice that records $49.00.
  • Leads counted twice. Dub dedupes on customerExternalId plus eventName, so look for two signup paths sending different event names, like "Sign Up" and "Signup", or one sending an email where the other sends the user ID.
  • A refund didn't void the commission. Look at the payout status. Dub's refunds docs say "Commissions that are already paid cannot be refunded" (refunds docs). For disputes, the Stripe docs go further: Dub doesn't change a commission "after its payout starts processing or is paid" (Stripe docs). Assume the same for refunds until your test shows otherwise.
  • Real partners got emails during testing. Staging payouts still email partners. Use test addresses only.

Other payment paths

Each of these needs its own trace. All from Dub's Stripe docs:

  • Stripe Customers. Put dubCustomerExternalId and dubClickId in the customer's metadata. Leads and sales are tracked, but lead rewards aren't generated.
  • Coupons and promotion codes. No code to write, but you lose geolocation, device, referrer and UTM data.
  • Payment Links. Add ?dub_client_reference_id=1 to the Payment Link when you shorten it on Dub, and Dub appends the click ID as client_reference_id for you. If visitors land on your marketing site first, append the dub_id yourself, prefixed with dub_id_. Lead webhooks and lead rewards aren't generated on this path.
  • Pricing Tables. Pass the click ID, prefixed with dub_id_, in the client-reference-id attribute.
  • Free trials. Dub can track subscription free trials as lead events; turn on Track Free Trials in the Stripe integration settings. Trace that run through to the first paid invoice.
  • Disputes. Holding a commission during an open dispute needs Advanced or above. "A lost dispute refunds the commission on every plan, the same as a Stripe refund," unless the payout is already processing or paid. For those, use a clawback.

What to keep

Save the filled-in table, the IDs and the date you ran it. Run the paid subscription and refund rows again whenever you change auth, checkout or domains.

If you're still picking a tool, I compared Dub vs Rewardful vs PartnerStack and Tolt alternatives on price. Once tracking passes, the next thing that bites is money: what happens to a partner's commission on refunds, mid-cycle upgrades and Black Friday coupons.

If you're choosing a tool for this, Dub is the one this guide is built around; the test needs its $90 Business plan. Affiliate link - I may earn a commission: https://refer.dub.co/shamil


Sources: Dub docs pages and Stripe's testing page, linked inline, read Nov 11, 2026. Checked against the docs, not run hands-on. All names and IDs are made up.

About the author

  • Independent notes on SaaS affiliate and referral tracking: pricing, Stripe attribution, refunds and payouts. Sourced from each vendor's own docs.

No comments yet. Be the first to comment.