# Leads and subscriptions

> Report leads, sales after a lead and subscription renewals from your server – and which integrations report renewals by themselves.

If your program pays per lead, or you sell subscriptions, your own server can report all of it to Affilyflow with the [store API with a key](https://www.affilyflow.com/en/docs/reference/conversion-api#the-store-api-with-a-key): leads, the sale that follows a lead, and every payment of a subscription. The rules are the same as for every other sale – cookie period, blocked affiliates, program rules, self-referral and sales held for review.

Every call needs the store's API key in `Authorization: Bearer afs_…`. Create it under **Settings → API keys** and keep it in an environment variable, e.g. `AFFILYFLOW_API_KEY`. Send `Idempotency-Key` with every `POST`, so a call can be retried after a timeout without counting twice – see [Idempotency](https://www.affilyflow.com/en/docs/reference/conversion-api#idempotency).

```http title="Endpoints"
POST https://app.affilyflow.com/api/v1/store/leads
GET  https://app.affilyflow.com/api/v1/store/leads/{lead_id}
POST https://app.affilyflow.com/api/v1/store/leads/{lead_id}/reject
POST https://app.affilyflow.com/api/v1/store/leads/{lead_id}/qualify
POST https://app.affilyflow.com/api/v1/store/conversions
```

## Who reports what?

| Integration | Leads | Subscription renewals |
|---|---|---|
| Your server (API key) | Yes – `POST /leads` | Yes – `POST /conversions` with `"renewal": true` |
| The tracking code (`data-lead-url`) | Yes – when the customer reaches the thank-you page | No – the browser doesn't see renewals |
| The Stripe webhook | No | Yes – every payment by itself, see [Stripe](https://www.affilyflow.com/en/docs/store/stripe) |
| WooCommerce Subscriptions | No | Yes – the plugin from version 1.14.0, see [WooCommerce](https://www.affilyflow.com/en/docs/store/woocommerce#subscriptions) |
| The Shopify app | No | No |
| Magento, PrestaShop, Shopware, Smartweb, DanDomain, Shoporama, Webflow | Through the tracking code | Only from your server with the API |

## Leads from your server

A lead from the server works across devices and can be qualified or rejected afterwards. It is for lead programs only – a program that pays per sale answers `422 not_lead_program`.

```bash title="POST /api/v1/store/leads"
curl -X POST https://app.affilyflow.com/api/v1/store/leads \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Idempotency-Key: LEAD-2026-0042" \
  -H "Content-Type: application/json" \
  -d '{"lead_id":"LEAD-2026-0042","afid":"c_AbC123xYz_-9","occurred_at":"2026-11-13T09:30:00Z"}'
```

| Field | Required | Meaning |
|---|---|---|
| `lead_id` | Yes | Your own ID for the lead: 1–100 printable ASCII characters without spaces, e.g. a number or a UUID. Never an email address or other personal data. |
| `afid` | When there is a click | The click ID (`c_…`). Alias: `click_id`. |
| `coupons` | No | The customer's discount codes as a list. Without a valid click, a [personal discount code](https://www.affilyflow.com/en/docs/store/personal-discount-codes) attributes the lead. |
| `value`, `currency` | No | A value for the lead, e.g. an estimated order value, and its currency. The commission is always the program's fixed amount per lead. |
| `occurred_at` | Recommended | When the lead happened (ISO 8601). The cookie period is measured from it. Not in the future and at most 180 days old. |
| `buyer_hash`, `buyer_hash_n` | Recommended | The customer's fingerprint, as for sales – see [Buyer fingerprint](https://www.affilyflow.com/en/docs/reference/conversion-api#buyer-fingerprint). |
| `test` | No | `true` = a test lead on the store's test link. Costs nothing and isn't counted. |

- **One lead per `lead_id`.** The same `lead_id` again gives `200 duplicate` and isn't counted twice.
- **At most one lead per click.** Another lead on the same click gives `422 lead_per_click`. If the same visitor can become several leads (e.g. several bookings), turn on **Several leads per click** under **Program → Terms → Leads and sales**. A rejected lead doesn't count.
- **The program's terms apply.** The lead gets the program's fixed amount per lead and its approval period, and follows the same rules as a sale.

The response is `201` with `status: "ok"` and the lead as `GET` shows it (`id`, `status`, `test`, `created_at`, `approval_due_at`, `commission`, `network_fee`, `attribution`, `confirmed_by_server`, `kind`, `lead_id`, `lead_qualified_at`, `awaiting_qualification` and more). A lead that got `422` is not recorded – the codes are in [Status codes](https://www.affilyflow.com/en/docs/reference/status-codes#leads-and-subscriptions).

### Look up a lead

```bash title="GET /api/v1/store/leads/{lead_id}"
curl https://app.affilyflow.com/api/v1/store/leads/LEAD-2026-0042 \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY"
```

An unknown `lead_id` gives `404 not_found`. Look up a test lead with `?test=true`.

### Reject an invalid lead

```bash title="POST /api/v1/store/leads/{lead_id}/reject"
curl -X POST https://app.affilyflow.com/api/v1/store/leads/LEAD-2026-0042/reject \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"duplicate","reason":"The same customer already asked for a quote"}'
```

- `code` is one of `duplicate`, `test_order`, `other_source`, `invalid_traffic`, `not_delivered` or `other` (the default). `other` needs a `reason` of at least 3 characters. `reason` is at most 300 characters, and the affiliate sees it.
- Only a pending lead within the approval period can be rejected – or a lead flagged for review. The affiliate is notified.
- The response is `200` with `status: "rejected"` (or `already_rejected`) and the lead. An approved lead gives `409 already_reviewed`, a lead Affilyflow is reviewing `422 held_by_affilyflow`, and a lead after the approval period `422 reject_window`.

### Leads that must be qualified

If you turn on **Leads must be qualified** under **Program → Terms → Leads and sales**, every new lead waits until you have qualified it – it is never approved automatically before that. Qualify it with the API or with **Approve** under **Sales** in the dashboard.

```bash title="POST /api/v1/store/leads/{lead_id}/qualify"
curl -X POST https://app.affilyflow.com/api/v1/store/leads/LEAD-2026-0042/qualify \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY"
```

The response is `200` with `status: "qualified"` (or `already_qualified`) and the lead. A rejected lead gives `409 already_rejected`. A qualified lead then follows the approval period like every other.

### Leads from the website

The tracking code can also report a lead without code when the customer reaches your thank-you page (`data-lead-url`, see [Leads without code](https://www.affilyflow.com/en/docs/reference/track-js#leads-without-code)). It reports at most one lead per visit, and the same one-lead-per-click rule applies.

## Lead and sale

Under **Program → Terms → Leads and sales**, choose what a lead program pays for:

| Choice | The lead | A later sale |
|---|---|---|
| **Leads only** (default) | The program's fixed amount | No commission |
| **Lead + sale** | The program's fixed amount | The program's sale commission (percentage or fixed amount) |
| **Sale only** | Recorded without commission | The program's sale commission |

- The sale is linked to the lead through **the same click** – whether the sale is reported by the tracking code, a plugin, Stripe or your server – or through `lead_id` on `POST /conversions`.
- The sale commission is locked on the click, like the program's rate.
- A sale after a lead is measured from **the lead**: if the sale comes within the cookie period after the lead, it earns commission.

```bash title="The sale after a lead"
curl -X POST https://app.affilyflow.com/api/v1/store/conversions \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Idempotency-Key: 1042" \
  -H "Content-Type: application/json" \
  -d '{"order":"1042","total":2400,"currency":"DKK","lead_id":"LEAD-2026-0042"}'
```

With `lead_id` you don't need to send the click again. If the lead doesn't exist, the answer is `422 lead_not_found` (a test lead only matches with `"test": true`); if it is rejected, `422 lead_rejected`; and if the program pays per lead only, `422 lead_only_program`.

## Subscriptions from your server

If you sell subscriptions outside Stripe, your server reports every payment to `POST /conversions`. You choose the number of months that earn commission under **Recurring commission (subscriptions)** on **Program** (6, 12 or 24 months or lifetime), and it is locked on the click.

**The first payment** is an ordinary sale with the click (or the customer's discount codes) and the subscription's ID:

```bash title="First payment"
curl -X POST https://app.affilyflow.com/api/v1/store/conversions \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Idempotency-Key: INV-5001" \
  -H "Content-Type: application/json" \
  -d '{"order":"INV-5001","total":199,"currency":"DKK","afid":"c_AbC123xYz_-9","subscription_id":"SUB-881","period":"2026-11"}'
```

**Every renewal** is sent with `"renewal": true` and the same `subscription_id` – without a click:

```bash title="Renewal"
curl -X POST https://app.affilyflow.com/api/v1/store/conversions \
  -H "Authorization: Bearer $AFFILYFLOW_API_KEY" \
  -H "Idempotency-Key: INV-5114" \
  -H "Content-Type: application/json" \
  -d '{"order":"INV-5114","total":199,"currency":"DKK","subscription_id":"SUB-881","renewal":true,"period":"2026-12"}'
```

| Field | Meaning |
|---|---|
| `subscription_id` | Your own ID for the subscription, at most 120 characters. Never an email address. |
| `renewal` | `true` on a renewal. Needs `subscription_id`, and a renewal can't have a `lead_id`. |
| `period` | Optional: the period the payment covers, e.g. `"2026-11"`. At most 40 characters. Two renewals for the same period with different order numbers aren't counted twice. |

- The rules are the same as for Stripe: the number of months is locked on the click, a subscription earns commission until the cap is reached, and if the first payment is rejected, the renewals earn no commission either.
- Commission is calculated from `total` – the goods after discounts, excluding VAT and shipping – as for a sale.
- Every renewal is its own sale with its own order number. It is refunded on its own with the [refunds endpoint](https://www.affilyflow.com/en/docs/reference/conversion-api#refunds-from-your-server).
- The response to a recorded renewal is `201 ok` with `cycle` (which payment it is) and `cap` (the cap from the click).
- With `"test": true` all of it works as test sales, renewals included.

| `status` | When |
|---|---|
| `not_recurring` | The program doesn't pay commission on renewals. |
| `cap_reached` | The commission period for this subscription is over. |
| `first_rejected` | The subscription's first payment was rejected. |
| `unknown_subscription` | No first payment with this `subscription_id` in your store. Send the first payment with `subscription_id` and the click first. |
| `duplicate_period` | The subscription already has a renewal for this period (with another order number). |
| `subscription_exists` | The subscription already has a first payment. Send the next ones with `"renewal": true`. |

## Code snippets

The code snippets under **Custom-built store** in Setup and in [Server-side tracking](https://www.affilyflow.com/en/docs/store/server-side-tracking) have the functions ready: in PHP `affilyflow_lead(…)` and `affilyflow_sale(…)` with the named arguments `subscriptionId:`, `renewal:`, `period:` and `leadId:`, and in JavaScript/TypeScript `affilyflowLead({ leadId, click, coupons, email, value, currency, occurredAt })` and `affilyflowSale({ …, subscriptionId, renewal, period, leadId })`.

See also [Conversion API](https://www.affilyflow.com/en/docs/reference/conversion-api#the-store-api-with-a-key), [Status codes](https://www.affilyflow.com/en/docs/reference/status-codes) and [Approval and refunds](https://www.affilyflow.com/en/docs/store/approval-and-refunds).

Source: https://www.affilyflow.com/en/docs/store/leads-and-subscriptions
