> ## Documentation Index
> Fetch the complete documentation index at: https://developers.tapfiliate.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pay recurring commissions from your billing system

> Create a customer when someone signs up through an affiliate, then report every payment, so the affiliate earns on renewals too.

Use this recipe when you sell subscriptions and want to pay the referring affiliate for every payment, not only the first one. It works with any billing system that tells your server when a payment succeeds.

The idea: a **customer** ties a person in your product to the affiliate who referred them. You create the customer once, when they sign up. After that, every payment you report with the same `customer_id` goes to that affiliate. Renewals need no click and no cookie.

## Before you start

Check three settings in your program.

* **Enable lifetime/recurring commissions** is on, in the program's commission settings. Without it, `customer_id` alone doesn't attribute a payment.
* **Recurring frequency cap** and **Recurring time cap** are set the way you want, or left empty for no limit. See [Limit how long an affiliate earns](#limit-how-long-an-affiliate-earns).
* **Recurring commissions** on the commission type is set to **Smart connected**. This means renewals come from your integration. The other option, **Time based**, makes Tapfiliate create renewals itself, see [Let Tapfiliate generate renewals](#let-tapfiliate-generate-renewals).

You can read the first two through the API: [Retrieve a program](/api-reference/programs/retrieve-a-program) returns `recurring`, `recurring_cap` and `recurring_period_days`.

## Steps

<Steps>
  <Step title="Create the customer at sign-up">
    When a visitor who came through an affiliate signs up, create a customer from your server. Capture the affiliate's click in the browser first, as in step 1 of [Track sales from a custom checkout](/recipes/custom-checkout).

    ```bash theme={null}
    curl -X POST https://api.tapfiliate.com/1.6/customers/ \
      -H "X-Api-Key: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json" \
      -d '{
        "customer_id": "USER789",
        "click_id": "CLICK_ID_FROM_THE_SIGNUP",
        "status": "trial"
      }'
    ```

    | Field | Description |
    | - | - |
    | `customer_id` | Your ID of this person or account, the one your billing system knows. Unique in your Tapfiliate account. |
    | `click_id` or `tracking_id` | Who referred them. `referral_code`, `coupon` or `asset_id` with `source_id` work too, with the same [precedence](/recipes/custom-checkout#which-identifier-wins). |
    | `status` | `trial` for subscriptions, `lead` for lead generation, `new` otherwise. The default is `new`. |

    No commission is created yet. The response is the customer. Save its `id` (`cu_…`) next to your user: you need it to cancel the customer later.

    ```json theme={null}
    {
      "id": "cu_0Th3r",
      "customer_id": "USER789",
      "status": "trial",
      "affiliate": { "id": "janejameson", "firstname": "Jane", "lastname": "Jameson" }
    }
    ```

    The cookie time is checked here, at sign-up. Payments that come later are not limited by it.

    If the visitor came without an affiliate, don't create a customer. You will skip their payments in the next step.
  </Step>

  <Step title="Report every successful payment">
    Each time your billing system confirms a payment of a referred customer, create a conversion with the same `customer_id`. Send the payment's ID as `external_id` and the amount actually charged.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.tapfiliate.com/1.6/conversions/ \
        -H "X-Api-Key: YOUR_API_KEY" \
        -H "Content-Type: application/json" \
        -H "Accept: application/json" \
        -d '{
          "customer_id": "USER789",
          "external_id": "INV-2026-0042",
          "amount": 49.00,
          "currency": "EUR",
          "commission_type": "renewal"
        }'
      ```

      ```js Node.js theme={null}
      const API = 'https://api.tapfiliate.com/1.6';

      async function tapfiliate(method, path, body) {
        const response = await fetch(API + path, {
          method,
          headers: {
            'X-Api-Key': process.env.TAPFILIATE_API_KEY,
            'Content-Type': 'application/json',
            Accept: 'application/json',
          },
          body: body ? JSON.stringify(body) : undefined,
        });
        const data = await response.json().catch(() => null);
        return { ok: response.ok, status: response.status, data, message: data?.errors?.[0]?.message ?? '' };
      }

      // Sign-up: only for visitors who came through an affiliate.
      async function onSignup(user) {
        if (!user.affiliateClickId) return;

        const res = await tapfiliate('POST', '/customers/', {
          customer_id: String(user.id),
          click_id: user.affiliateClickId,
          status: 'trial',
        });
        if (!res.ok) throw new Error(`Tapfiliate ${res.status}: ${res.message}`);

        await saveTapfiliateCustomerId(user.id, res.data.id); // "cu_…", needed to cancel
      }

      // Billing webhook: a payment succeeded.
      async function onPaymentSucceeded(payment) {
        const user = await findUser(payment.userId);
        if (!user.tapfiliateCustomerId) return; // not referred by an affiliate

        const res = await tapfiliate('POST', '/conversions/', {
          customer_id: String(user.id),       // must be a string
          external_id: payment.id,            // unique per payment
          amount: payment.amount,
          currency: payment.currency,
          commission_type: payment.isFirst ? 'first-payment' : 'renewal',
        });
        if (res.ok) return res.data;

        // Already tracked, for example by an earlier attempt.
        if (res.message.startsWith('Duplicate conversion')) return null;
        // The customer is canceled or reached the program's cap: no more commissions.
        if (res.message.startsWith('Customer is capped')) return null;
        if (res.message.startsWith('Recurring cap or recurring period exceeded')) return null;

        throw new Error(`Tapfiliate ${res.status}: ${res.message}`);
      }

      // Billing webhook: the subscription ended.
      async function onSubscriptionCanceled(user) {
        if (!user.tapfiliateCustomerId) return;
        await tapfiliate('DELETE', `/customers/${user.tapfiliateCustomerId}/status/`);
      }
      ```
    </CodeGroup>

    | Field | Description |
    | - | - |
    | `customer_id` | The ID from step 1, as a JSON string. A number is rejected with `400`. |
    | `external_id` | The ID of this payment or invoice. It must be new for each payment, see [retries](/recipes/custom-checkout#retries-and-duplicates). |
    | `amount` | The amount of this payment. Commissions are calculated from it, so a plan change is reflected automatically. |
    | `currency` | Three-letter ISO code of `amount`. Without it, the program currency is used. |
    | `commission_type` | Optional. The identifier of the commission type to use, see [different rates](#a-different-rate-for-the-first-payment). Without it, the program's default type is used. |

    Tapfiliate finds the customer by `customer_id` and gives the commission to the customer's affiliate. Other identifiers in the request are ignored. The response is the conversion with its commissions and a `customer` object.

    The first conversion moves the customer from `trial` to `paying` (from `new` to `active`, from `lead` to `won`).
  </Step>

  <Step title="Cancel the customer when the subscription ends">
    Use the `cu_…` ID you saved in step 1. If you don't have it, find it with `GET /customers/?customer_id=USER789`.

    ```bash theme={null}
    curl -X DELETE https://api.tapfiliate.com/1.6/customers/cu_0Th3r/status/ \
      -H "X-Api-Key: YOUR_API_KEY" \
      -H "Accept: application/json"
    ```

    The response is the customer with status `canceled` (`lost` for leads, `stopped` otherwise). After that, a conversion with this `customer_id` fails with `400 Customer is capped. No new conversions will be generated for this customer.` Existing commissions are not changed.

    If the person subscribes again, uncancel the customer with `PUT` on the same URL. The original affiliate keeps earning:

    ```bash theme={null}
    curl -X PUT https://api.tapfiliate.com/1.6/customers/cu_0Th3r/status/ \
      -H "X-Api-Key: YOUR_API_KEY" \
      -H "Accept: application/json"
    ```

    <Warning>
      Cancel with `DELETE /customers/{id}/status/`. `DELETE /customers/{id}/` without `status/` deletes the customer permanently.
    </Warning>
  </Step>
</Steps>

## A different rate for the first payment

Tapfiliate doesn't decide which payment is the first. The commission type you send does. To pay, for example, a fixed amount for the first payment and a percentage of each renewal:

1. In the program, create two commission types. Give them identifiers, for example `first-payment` (fixed) and `renewal` (percentage). Identifiers use lowercase letters, digits and dashes.
2. Send `"commission_type": "first-payment"` with the first payment and `"commission_type": "renewal"` with every later one. Your billing system knows which payment is which.

[List program commission types](/api-reference/programs/list-program-commission-types) returns the identifiers of a program's types.

An identifier that doesn't match an active commission type in the program is not an error. The program's default type is used instead, so check the commissions in the response while you test. Affiliate group overrides still apply to the type you send.

To pay only for the first payment, report only the first payment. Turning recurring commissions off doesn't do it: a later payment sent with another identifier, such as `referral_code`, still creates a commission.

## Limit how long an affiliate earns

Two program settings stop commissions for a customer. Leave them empty for no limit.

* **Recurring frequency cap** — the number of conversions per customer. The first payment counts too. Conversions whose commission type has **Count recurring commissions towards cap** turned off are not counted. Disapproved commissions still count.
* **Recurring time cap** — the number of days from the moment the customer was created. With a free trial, the trial is included.

With a frequency cap of 12, the 12th counted conversion is still created, and the customer's status becomes capped, for example `paying_capped`. Later conversions fail with `400 Customer is capped. No new conversions will be generated for this customer.`

Once the time cap has passed, the next conversion fails with `400 Recurring cap or recurring period exceeded for this customer`, and the customer becomes capped too.

Raising the cap later doesn't reopen customers who are already capped. To reopen one, uncancel it with `PUT /customers/{id}/status/`.

## Let Tapfiliate generate renewals

If you can't report each payment, set **Recurring commissions** on the commission type to **Time based** and pick a **Recurrence interval**: weekly, monthly or yearly. You still create the customer and send the first payment with this commission type, as in steps 1 and 2. From then on, Tapfiliate creates a conversion for the customer every interval by itself, until you cancel the customer or a cap is reached.

Each generated conversion repeats the amount and the commission type of the first payment. Its `external_id` starts with `autorecur-`. Upgrades, downgrades, failed payments and refunds are not reflected, so report payments yourself whenever your billing system can.

<Warning>
  Don't combine the two. Payments you report don't move the schedule of a Time based commission type, so each renewal would be paid twice: once by your conversion and once by the generated one.
</Warning>

## Refunds and corrections

* **Refund of one payment.** Disapprove the commission of that payment with [Disapprove a commission](/api-reference/commissions/disapprove-a-commission). Its ID is in `commissions[].id` of the conversion.
* **Partial refund.** Change the conversion's `amount` with [Update a conversion](/api-reference/conversions/update-a-conversion) and `?recalculate_commissions=true`.
* **Subscription ended.** Cancel the customer, step 3.

On accounts that use Tapfiliate Pay, an approved commission can't be disapproved: the API answers `409 commission_approved`, see [Errors](/api-reference/errors#conflicts).

## Customer statuses

The status you choose at creation sets the kind of customer. Statuses then change like this:

| Created as | After the first conversion | Limit reached | Canceled |
| - | - | - | - |
| `trial` | `paying` | `paying_capped` | `canceled` |
| `lead` | `won` | `won_capped` | `lost` |
| `new` | `active` | `active_capped` | `stopped` |

Uncanceling returns the customer to its active status, to its capped status if the frequency cap is reached, or to its initial status if it has no conversions yet.

`PUT /customers/{id}/status/` on a customer that is still `trial`, `lead` or `new` fails with `400`. Such a customer becomes active only through a conversion.

## Common pitfalls

* **`Missing tracking parameter value and no customer found for customer id: …`.** The customer doesn't exist. It was never created, or the payment uses a different `customer_id` than the sign-up, for example the billing system's own customer ID instead of yours. Send the same value in both places.
* **`Missing tracking parameter value and no lifetime/recurring commissions enabled in program: …`.** The customer exists, but **Enable lifetime/recurring commissions** is off in its program.
* **The same `external_id` for every payment.** The second payment fails with `Duplicate conversion`. Use the payment or invoice ID, not the subscription ID.
* **A returning customer.** A canceled customer stays canceled, even if the person comes back through a new affiliate link. Uncancel it to keep paying the original affiliate.
* **Renewals without `customer_id`.** A renewal sent with only a click, tracking ID or referral code is attributed like any new sale. A click or tracking ID must be within the cookie time, and the commission may go to another affiliate.
* **Turning recurring commissions off.** Customers with a Time based schedule are canceled, and `customer_id` alone stops attributing payments.

More messages and what to do about them are in [Errors](/api-reference/errors#common-errors-when-tracking).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.