Browse Product guides

Affiliates with Stripe

Use this guide when your website sends buyers to Stripe Checkout or a Stripe Payment Link created outside Plandalf. For a Plandalf-created checkout, use the Plandalf checkout guide.

Note. Early access These instructions describe the current affiliate preview. Verify a test payment and, for subscriptions, a paid renewal before switching to live mode.

1. Connect the Stripe sales source

  1. Connect the Stripe account that creates the payments under Settings → Integrations.
  2. Open Affiliates → Settings → General → Where customers pay.
  3. Enable Also credit sales made directly in Stripe, then save. This switch enables both test and live Stripe source flags; it is not a test-only switch. Use the connected Stripe test account and test prices for testing. The Affiliates Test mode switch filters the dashboard; it does not change Stripe payment mode.
  4. Check the source’s last received event after your test payment. The connected account’s webhook events must reach Plandalf; installing the browser script alone does not import sales.

This connection reads sales and subscription events. Stripe affiliate payouts are disabled in this release, so affiliates can still be paid manually or through PayPal.

2. Carry the referral into Stripe

Copy the script URL from Affiliates → Setup. Replace acme.plandalf.dev below with your organization’s host. Pick the checkout type you use.

Install this on the page containing your Payment Link:

<script
  src="https://acme.plandalf.dev/js/plandalf-sdk.js"
  data-config-referral-decorate="buy.stripe.com"
  async
></script>

<a href="https://buy.stripe.com/REPLACE_WITH_YOUR_PAYMENT_LINK">Buy now</a>

Open that page through a partner link before clicking Buy now. The SDK captures the referral and adds it as Stripe’s client_reference_id when the link is clicked. Use an ordinary link to buy.stripe.com for this flow. Programmatic redirects and custom Stripe domains need an explicit handoff.

The decorator preserves an existing client_reference_id. If you already use that field for something else, use a server-created Checkout Session with referral metadata instead. Do not put one visitor’s referral token into a shared Payment Link’s permanent metadata.

Stripe Checkout created by your server

Read the referral before calling your own checkout endpoint. This example loads the SDK before the inline setup code; install the SDK only once.

<button id="buy" disabled>Buy now</button>
<script src="https://acme.plandalf.dev/js/plandalf-sdk.js"></script>
<script>
  plandalf("ready", (sdk) => {
    const button = document.querySelector("#buy");
    button.disabled = false;
    button.addEventListener("click", async () => {
      button.disabled = true;
      try {
        const referral = await sdk.referral();
        const response = await fetch("/billing/checkout", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ referral_token: referral?.token ?? null }),
        });
        if (!response.ok) throw new Error("Could not start checkout");
        const { url } = await response.json();
        window.location.assign(url);
      } catch (error) {
        button.disabled = false;
        window.alert("Checkout could not start. Please try again.");
      }
    });
  });
</script>

/billing/checkout is an endpoint you implement in your app, not a Plandalf API route. Apply your normal authentication and CSRF protection there. Get the price and Stripe customer from trusted server-side records, not from the browser request. Pass the captured referral_token as referralToken to the following helper.

For a one-off payment, set metadata on the Session and PaymentIntent:

export async function createOneOffCheckout(stripe, { referralToken, stripeCustomerId }) {
  const metadata = referralToken ? { plandalf_referral: referralToken } : {};
  return stripe.checkout.sessions.create({
    mode: "payment",
    customer: stripeCustomerId,
    line_items: [{ price: process.env.STRIPE_ONE_OFF_PRICE_ID, quantity: 1 }],
    success_url: "https://your-site.com/thanks",
    cancel_url: "https://your-site.com/pricing",
    metadata,
    payment_intent_data: { metadata },
  });
}

For a subscription, set metadata on the Session and the Subscription:

export async function createSubscriptionCheckout(stripe, { referralToken, stripeCustomerId }) {
  const metadata = referralToken ? { plandalf_referral: referralToken } : {};
  return stripe.checkout.sessions.create({
    mode: "subscription",
    customer: stripeCustomerId,
    line_items: [{ price: process.env.STRIPE_RECURRING_PRICE_ID, quantity: 1 }],
    success_url: "https://your-site.com/thanks",
    cancel_url: "https://your-site.com/pricing",
    metadata,
    subscription_data: { metadata },
  });
}

Use your initialized Stripe server client and real success/cancel URLs. Keep Stripe secret keys on the server. Plandalf validates the referral evidence; the browser does not decide the affiliate or commission rate.

Session metadata alone is not automatically copied to all related Stripe objects. subscription_data.metadata puts it on the Subscription, and Stripe snapshots that metadata onto later invoices. Stripe’s metadata rules.

If you create Subscriptions directly, attach metadata.plandalf_referral to the Subscription and retain the same Stripe customer ID. Standalone payment_intent.succeeded and charge.succeeded events do not currently create affiliate sales. For a PaymentIntent-only integration, report confirmed payments through the sales API.

3. Verify a paid sale

  1. Use a regular active partner’s link from Affiliates → Partners, with a fresh test customer whose email differs from the partner’s. The reserved plandalf-test link checks tracking but skips commission accrual and does not establish a recurring customer binding.
  2. Complete a payment through the actual Payment Link or Checkout Session using Stripe test mode.
  3. Open Affiliates → Sales in test mode. Check the affiliate, amount and commission decision.
  4. Confirm one payment creates one sale. A checkout-completed page alone is not proof of paid-sale tracking.
  5. For a subscription, generate a second paid test invoice without clicking the referral link again. Confirm the same affiliate receives the renewal under your subscription policy.

Do not additionally send these payments to the affiliate sales API. Use one reporting path per payment.

Events handled

The current sale-producing events are checkout.session.completed for paid one-off Checkout Sessions, and invoice.paid / invoice.payment_succeeded for paid invoices. The source also handles supported refund, credit-note and dispute events. Object links connect those adjustments to sales; they are not a guarantee that every Stripe object for the same payment is deduplicated into one sale.

Subscription Checkout Sessions supply attribution evidence but do not create a second sale. The paid invoice is the sale source. Put referral metadata on the Subscription for server-created Sessions. For subscription Payment Links, attribution depends on matching Session evidence already having arrived when the invoice is processed. The current source does not wait for a late Session or automatically re-attribute an invoice already recorded as direct. Test invoice-first delivery before relying on Payment Links for recurring attribution.

For delayed payment methods, an unpaid checkout.session.completed event is ignored. checkout.session.async_payment_succeeded is not currently handled by this affiliate source. Use a confirmed-payment API integration for those payments, and avoid also importing the same payment through another sale path.

Recognised Plandalf checkout metadata keeps Plandalf-created checkout sales on their native path. Do not additionally report those purchases through the affiliate sales API.

Attribution sources

Plandalf checks Stripe evidence including:

  • client_reference_id, including Payment Links decorated by the SDK
  • metadata.plandalf_referral
  • the program compatibility metadata key
  • subscription metadata
  • customer metadata when the customer object is expanded in the received payload
  • Stripe promotion-code, coupon or code mappings assigned to partners
  • existing customer bindings
  • direct partner codes where supplied by normalized input

Renewals and money events

Recurring commissions follow the saved duration policy: first payment only, a set period, or ongoing payments. See subscription setup for customer identity, trials, and renewal testing. Renewals use the customer binding while it is active, and the locked binding rate is used for recurring customer decisions.

Refunds, credit notes and disputes are processed as money adjustments:

  • Each successful partial refund is recorded by refund ID. Failed or cancelled refunds do not reverse commission.
  • Credit notes reverse on an ex-tax basis where Stripe provides one. A voided credit note restores the previous credit-note reversal.
  • Open disputes hold unpaid commissions.
  • A dispute with status lost reverses commission. won and warning_closed release the hold.
  • A funds-withdrawn event is processed according to its dispute status; it does not itself guarantee a reversal. charge.dispute.funds_reinstated is not currently handled. Review these cases before paying affected commissions.

Multi-currency sales are decided into the program payout currency with an FX snapshot. If the exchange rate is missing, the commission waits in a held state until the rate can be resolved.

Test mode and backfill

Stripe livemode is matched to the program’s live/test source settings. Test events stay in test mode.

Import past sales queues a backfill for the first connected Stripe integration found in the selected test/live mode, not every connected account. The range is 1–365 days and is based on object creation time. It scans completed paid Checkout Sessions, paid invoices, refunded charges, refunds, credit notes and disputes. Check account selection before using it if you have multiple connections.

The current backfill client requires a configured platform Stripe key and the integration’s connected-account ID. It does not use the merchant OAuth token as its primary credential. A connected account in the UI alone does not prove backfill is ready.

Per-stream checkpoints record progress and errors, but a retry scans the requested window again; it does not resume from the saved cursor. Backfill is not a reconstruction of every historical state transition. Review voided credit notes and resolved disputes against Stripe before relying on imported balances.

Failed and pending Stripe source events are retried by the source-event sweep. Pending adjustments, including refunds, credit notes and disputes that arrived before the matching sale link, are replayed after the sale is linked.

Feature detail