> ## 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.

# Track sales from a custom or third-party checkout

> Keep the affiliate's click with the order and create the conversion from your server when the payment succeeds.

Use this recipe when the sale happens somewhere tapfiliate.js can't report it: in your own backend, in an external checkout or billing system, or on another domain.

The idea is simple. The browser records the affiliate's click. You carry the click to the place that learns about the payment. That place creates the conversion through the API.

<Steps>
  <Step title="Record the click in the browser">
    Add tapfiliate.js to every page where affiliate links can land, as described in [JavaScript tracking](/tracking/javascript). Then keep an identifier of the click. There are two, pick one.

    <Tabs>
      <Tab title="Click ID">
        When `detect` records a click, its callback receives the click. Store `click.id` in your own first-party cookie:

        ```html theme={null}
        <script src="https://script.tapfiliate.com/tapfiliate.js" type="text/javascript" async></script>
        <script type="text/javascript">
          (function(t,a,p){t.TapfiliateObject=a;t[a]=t[a]||function(){
          (t[a].q=t[a].q||[]).push(arguments)}})(window,'tap');

          tap('create', 'YOUR_ACCOUNT_ID');
          tap('detect', {}, function (click) {
            // Runs only when the visitor arrived through an affiliate link.
            document.cookie = 'affiliate_click_id=' + encodeURIComponent(click.id) +
              '; path=/; max-age=2592000; SameSite=Lax';
          });
        </script>
        ```

        The callback runs only on the page view where the click is recorded. A visitor who returns later without an affiliate link keeps the cookie you set.

        A click ID points to exactly one click. Prefer it when you can store it.
      </Tab>

      <Tab title="Tracking ID">
        `getTrackingId` returns the visitor IDs that tapfiliate.js keeps in this browser, joined with `__`. Call it on any page that runs the snippet, after `tap('create', …)`, for example on the checkout page:

        ```js theme={null}
        tap('getTrackingId', {}, function (trackingId) {
          // null if this browser has no Tapfiliate visitor ID.
          if (trackingId) {
            document.getElementById('affiliate_tracking_id').value = trackingId;
          }
        });
        ```

        When you send a tracking ID, Tapfiliate looks up the visitor's most recent click. If there is none, the conversion fails with `No click found for visitor id`.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Carry it to the checkout">
    The ID has to reach the system that will know about the payment.

    * **Your own backend.** Read the cookie on the request that creates the order, or send the ID in a hidden form field. Save it on the order.
    * **An external checkout or billing system.** If it lets you attach your own data to a payment and sends that data back in its payment notification, put the ID there. A URL parameter on the checkout link or a hidden field in the checkout form works the same way.
    * **Another domain.** Cookies don't travel between domains. Add the ID to the link or the form that takes the visitor there.

    ```html theme={null}
    <form method="post" action="/checkout">
      <input type="hidden" name="affiliate_click_id" id="affiliate_click_id">
      <!-- your checkout fields -->
    </form>
    <script>
      var match = document.cookie.match(/(?:^|; )affiliate_click_id=([^;]*)/);
      if (match) document.getElementById('affiliate_click_id').value = decodeURIComponent(match[1]);
    </script>
    ```

    No ID means Tapfiliate recorded no affiliate click for this visitor. Don't send a conversion for that order.
  </Step>

  <Step title="Create the conversion when the payment succeeds">
    From your server, send the stored ID with your order ID and amount. Use `click_id` or `tracking_id`, whichever you stored.

    <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 '{
          "click_id": "CLICK_ID_FROM_THE_ORDER",
          "external_id": "ORD123",
          "amount": 49.99,
          "currency": "EUR"
        }'
      ```

      ```js Node.js theme={null}
      // Call this from the handler that learns the payment has succeeded.
      async function trackConversion(order) {
        if (!order.affiliateClickId) return null; // not referred by an affiliate

        const response = await fetch('https://api.tapfiliate.com/1.6/conversions/', {
          method: 'POST',
          headers: {
            'X-Api-Key': process.env.TAPFILIATE_API_KEY,
            'Content-Type': 'application/json',
            Accept: 'application/json',
          },
          body: JSON.stringify({
            click_id: order.affiliateClickId,
            external_id: order.id,
            amount: order.total,
            currency: order.currency,
          }),
        });

        if (response.ok) return response.json();

        const body = await response.json().catch(() => null);
        const message = body?.errors?.[0]?.message ?? '';

        // Already tracked, for example by an earlier attempt: nothing to do.
        if (message.startsWith('Duplicate conversion')) return null;

        throw new Error(`Tapfiliate ${response.status}: ${message}`);
      }
      ```
    </CodeGroup>

    | Field | Description |
    | - | - |
    | `click_id` or `tracking_id` | The ID you stored in step 1. |
    | `external_id` | Your order or payment ID. Unique per conversion in your account, see [retries](#retries-and-duplicates). |
    | `amount` | The order amount. Commissions are calculated from it. |
    | `currency` | Three-letter ISO code of `amount`. Without it, or with a code Tapfiliate doesn't know, the program currency is used. |

    A `200` response is the created conversion, with its affiliate and commissions. All fields are in [Create a conversion](/api-reference/conversions/create-a-conversion).
  </Step>

  <Step title="Confirm it worked">
    * Set up a webhook for the **Conversion created** event, see [Webhooks](/automation/webhooks). It fires for conversions created through the API.
    * Or look the conversion up by your order ID:

    ```bash theme={null}
    curl "https://api.tapfiliate.com/1.6/conversions/?external_id=ORD123" \
      -H "X-Api-Key: YOUR_API_KEY"
    ```

    Track a test sale through the whole flow before you launch.
  </Step>
</Steps>

## No JavaScript on the landing page

If affiliate links land where you can't run tapfiliate.js, create the click from your server. Read the affiliate's referral code from the landing URL (the `ref` parameter by default) and send it with the visitor's details:

```bash theme={null}
curl -X POST https://api.tapfiliate.com/1.6/clicks/ \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "referral_code": "janejameson",
    "landing_page": "https://example.com/pricing?ref=janejameson",
    "referrer": "https://janes-blog.example/review",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36",
    "ip": "203.0.113.7"
  }'
```

The response holds only the click ID:

```json theme={null}
{ "id": "5f0c7a2e-8b1d-11ef-9c3a-0242ac120002" }
```

Save it on the visitor's session or order and use it as `click_id` in step 3.

| Field | Description |
| - | - |
| `referral_code` | The affiliate's referral code. Instead, you can send `asset_id` and `source_id` from the affiliate's link. One of the two is required. |
| `landing_page`, `referrer` | The page the visitor landed on and where they came from. |
| `user_agent`, `ip` | The visitor's browser and IP address. Tapfiliate stores only what you send: it doesn't take them from your server's request. |

Errors:

* `404` — the referral code doesn't exist in your account, or the asset or source doesn't exist.
* `400` — neither a referral code nor `asset_id` with `source_id` was sent, `ip` isn't a valid IP address, or the affiliate isn't approved in the program. The body says only `Bad Request`, so check these three.
* `400 Program is archived` — the program no longer accepts clicks.

## Which identifier wins

You can send several identifiers in one conversion request. Tapfiliate uses exactly one, in this order:

1. `coupon`
2. `click_id`
3. `referral_code`
4. `asset_id` with `source_id`
5. `tracking_id`

If the winner can't be resolved, the request fails with `400`. Tapfiliate does not fall back to the next identifier. A `click_id` of another account, for example, fails with `Invalid click id` even when a valid `referral_code` is sent too.

One exception comes first: `customer_id` of an existing customer, in a program with recurring commissions, sends the conversion to that customer's affiliate.

## Choosing the program

The conversion never names a program. The identifier decides it:

* **`click_id` or `tracking_id`** — the program of the click.
* **`referral_code`** — an affiliate has a separate referral code in each program, so the code points to one program.
* **`coupon`** — the program the coupon is assigned to.

If your account uses program groups, send `program_group` with a `click_id` or `tracking_id`. Tapfiliate then accepts only clicks in that group's programs. With other identifiers `program_group` has no effect.

## Retries and duplicates

`external_id` is unique in your account. A second conversion with the same `external_id` fails with `400 Duplicate conversion. There is already a conversion for External id '…'`. That makes retries safe: treat this error as success.

A retry that arrives while an earlier attempt with the same `external_id` is still running, or within 10 seconds after one failed, gets `400 To many concurrent attempts to create conversion with id '…'` instead. Wait more than 10 seconds and retry.

Without `external_id` there is no duplicate check. Each retry, for example after a timeout, creates another conversion.

## Common pitfalls

* **Tracking the same sale twice.** The browser and the server share the duplicate check by `external_id`. If tapfiliate.js reports the sale with one ID and your server with another, for example the order ID and the payment ID, you get two conversions. This often happens with coupons: `tap('conversion')` with `coupons` and an API call with `coupon` for the same order. Track each sale in one place, or use the same `external_id` in both.
* **`Cookie expired`.** A conversion by `click_id` or `tracking_id` fails when the click is older than the program's cookie time. To track a late payment anyway, add `?override_max_cookie_time=true` to the request URL.
* **No identifier.** A request without `click_id`, `tracking_id`, `referral_code`, `coupon`, `asset_id` with `source_id` or `customer_id` fails with `Missing mandatory parameter(s)`.

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.