Conversion Tracking

Report signups and sales back to the Flyn link that earned them, from the browser tag or a server-to-server postback. Available on Pro and Lifetime, per link.

Overview

Conversion tracking answers the question a click count cannot: which link produced the signup or the sale. When a link has the toggle on, the redirect hands each human click an unguessable id and appends it to your destination as ?flyn_id=...; your site (or your affiliate network) sends that id back when the visitor converts, and the conversion, with its value, lands on the exact link that earned it.

The click id is the credential

There is no API key on this endpoint, by design: the ping comes from your own domain, which Flyn can never enumerate, so CORS is open. The click id is random, is minted per click, dies with the 30-day window, and the unique index on (click, event, txid) means a replayed ping collapses into the first row. No cookies are read and no visitor identity is accepted or stored. The endpoint records that a click converted, nothing else.

1. A human clicks

The redirect mints a per-click id and appends it to your destination as ?flyn_id=...

2. Your site stores it

The tag saves the id in a first-party cookie plus localStorage and cleans the address bar.

3. The goal reports back

Your thank-you page (or your server) sends the id to Flyn with the value of the sale.

4. It lands on the link

The conversion, its value and its currency appear on the exact link that earned it.

Turn it on for a link

Tracking is per link and off by default. Only links with it on carry a click id, so every other link's destination URL stays untouched.

  1. Create or edit a link, then flip Conversion tracking in the editor.
  2. Or set conversionTracking: true on POST /api/links or PATCH /api/links/:id.
Request
curl -X POST https://www.flyn.to/api/links \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "conversionTracking": true
  }'

Switching the toggle off silences the endpoint for that link immediately: pings then answer 404. The link object also returns conversions, conversionValueCents and conversionCurrency.

The browser tag

The tag is served from https://www.flyn.to/flyn.js. It is identical for every site, so there is no id to configure.

1. Site-wide, on every page

HTML
<script src="https://www.flyn.to/flyn.js" defer></script>

On any page load, if the URL carries ?flyn_id= the tag stores it in a first-party cookie and in localStorage, then removes the parameter from the address bar with history.replaceState. Last click wins. Storage is requested for 90 days, though Safari caps script-set storage nearer 7.

2. On the thank-you page

Use this variant instead of the plain one on the page a customer reaches after converting. Every data- attribute except data-conversion is optional.

HTML
<script src="https://www.flyn.to/flyn.js"
        data-conversion
        data-value="49.00"
        data-currency="USD"
        data-event="sale"
        data-txid="ord_10231"
        defer></script>

3. From your own code

Use this when the value is only known at runtime (a cart total, a plan price) or when the goal is not a page load at all.

JavaScript
window.flyn('conversion', {
  value: 49.00,
  currency: 'USD',
  event: 'sale',
  txid: 'ord_10231',
});

Call it after the tag has loaded

window.flyn does not exist until flyn.js has executed, and the snippets above use defer. Call it from a DOMContentLoaded handler, from a framework effect, or from an inline script placed after the tag. Calling it earlier throws a TypeError and the conversion is lost.

The call returns false when no click id is stored, which is the normal case for a visitor who did not arrive through a tracked link. It fires once per (event, click) per browser session, so a refreshed thank-you page does not re-send; the server dedupes as well. The body is sent via navigator.sendBeacon, falling back to fetch with keepalive, as a plain text body so the request stays a CORS simple request that needs no preflight.

The server postback

For affiliate networks and any backend that confirms the sale server-side. /api/postback is the same handler as /api/conversion under the name every tracker uses, so a postback URL template pasted from network docs works as-is.

POST/api/conversion
GET/api/conversion
GET/api/postback
POST/api/postback

This is runnable as-is once you swap in a real click id:

Request
curl "https://www.flyn.to/api/postback?click_id=V1StGXR8_Z5jdHi6&event=sale&value=49.00&currency=USD&txid=ord_10231"
Response
{
  "ok": true
}

The same call as JSON, for a backend that would rather POST:

Request
curl -X POST https://www.flyn.to/api/conversion \
  -H "Content-Type: application/json" \
  -d '{
    "click_id": "V1StGXR8_Z5jdHi6",
    "event": "sale",
    "value": 49.00,
    "currency": "USD",
    "txid": "ord_10231"
  }'

To wire this to a network, pass the click id into the network's subid field on your destination URL and put their click-id macro where click_id goes. POST accepts JSON, form-encoded and plain-text bodies; query parameters are merged underneath the body as a fallback, so both transports work.

Parameters

The same set for every transport: the tag, POST /api/conversion, and the GET postback.

ParameterTypeRequiredDescription
click_idstringYesThe id the redirect handed the click. Aliases: clickId, flyn_id, clickid, cid. Must match [A-Za-z0-9_-] and be 10 to 32 characters.
eventstringNoGoal name, lowercased. Default "conversion". Alias: type. 1 to 40 characters of a-z, 0-9, _ and -. Use distinct names for multi-step funnels ("signup", "purchase").
valuenumber | stringNoOrder value in major units (49.00, not 4900). Aliases: payout, sum, amt. Both "1,234.56" and "1.234,56" parse. Absolute values above 1,000,000 and unparseable values record as 0 rather than rejecting the ping. A negative value is a refund.
currencystringNoThree-letter code, uppercased. Default USD. Anything else (an unresolved macro, for instance) falls back to USD instead of losing the conversion.
txidstringNoYour transaction id, first 64 characters. Alias: transaction_id. Part of the dedupe key, so a distinct txid on the same click and event records a new conversion (upsell, rebill, refund).

There is deliberately no ref alias for event. Postback URLs commonly carry &ref= as a referral tag, which would silently rename the event and fragment the dedupe key into one conversion per distinct ref value.

Status codes

StatusBodyWhen
200{"ok":true}Recorded.
200{"ok":true,"duplicate":true}The same (click_id, event, txid) already exists. Retries and double fires collapse into the first row.
400{"ok":false,"error":"click_id missing or malformed"}No click id, or it failed the format check. Also returned when event is not 1 to 40 chars of a-z 0-9 _ -.
404{"ok":false,"error":"unknown click_id"}No click carries that id. Also 404, with a different message, when the link has conversion tracking switched off.
410{"ok":false,"error":"click is outside the 30-day attribution window"}The click is older than the attribution window.
429{"ok":false,"error":"Too many requests"}More than 60 pings from one IP in 60 seconds. Also 429, as "conversion limit reached for this click", at 25 conversions on a single click.
405Next.js defaultA method the route does not implement. Only GET, POST and OPTIONS exist.
503{"ok":false,"error":"temporarily unavailable"}The database call failed. Safe to retry: the unique index makes a retry idempotent.

The tag never reads the response, so a browser-side mistake is silent. When you are debugging an integration, send the same payload with curl and read the status.

Refunds

A refund is a conversion with a negative value. It subtracts from both the link's conversion count and its total value.

A refund needs its own event name or its own txid

Deduplication is a unique index on (click_id, event, txid). A refund sent with the same event and the same txid as the sale it reverses collides with that sale, comes back as {"ok":true,"duplicate":true}, and changes nothing. Send it as event=refund, or reuse the event name with a distinct txid.

Refund, its own event name
curl "https://www.flyn.to/api/postback?click_id=V1StGXR8_Z5jdHi6&event=refund&value=-49.00&currency=USD&txid=ord_10231"
Refund, same event, distinct txid
curl "https://www.flyn.to/api/postback?click_id=V1StGXR8_Z5jdHi6&event=sale&value=-49.00&currency=USD&txid=ord_10231_refund"

Keep the currency the same as the original sale. See the currency note under Limits for why.

Limits

30-day attribution window

A click older than 30 days can no longer convert: the endpoint answers 410. In the browser the practical window is shorter still on Safari and iOS, which cap script-set storage nearer 7 days. Server postbacks are unaffected by browser storage, only by the 30 days.

Human clicks only

The redirect mints a click id only when the visit passes Flyn's bot filter. A crawler never gets an id and therefore can never convert, which is also why conversion rate is computed against human clicks rather than raw traffic.

25 conversions per click

One click can produce at most 25 conversion rows across all events and txids. Past that the endpoint answers 429. The id is handed to the visitor, so without a ceiling one clicker could mint rows at will by varying event and txid.

One currency per link

The first currency a link reports becomes its currency of record. A second, different currency flips the link to a MIXED sentinel: values are summed, never exchange-rate converted, so a mixed link's total is not a number you should read as money. Report one currency per link.

The tag needs a page you control

The click id lives in first-party storage on the domain where the tag ran. A hosted checkout on someone else's domain (a payment provider's page, a marketplace, an app store) cannot read it and you cannot install the tag there. Two ways out: fire the conversion from your own site before the visitor leaves for checkout, or send the postback from your backend once the payment webhook confirms the sale.

60 pings per minute per IP

Rate limiting is per client IP across the endpoint. A backend firing conversions for many customers from one address should pace itself, and treat 429 and 503 as retryable: the unique index makes a retry idempotent.

Further reading

Was this page helpful? Spotted something wrong?