Tracking
Pass s1, s2 and s3 on any link and they come back on every report. Deep-link to any page with ?to=. Everything below is the server-to-server half: proving the URL is yours, and proving a postback is ours.
Turning your postback URL on
The token is never in the request that checks for it, so an endpoint that echoes its own query string back — a 404 page, a debug handler, half the APIs in the world — cannot pass this by accident. That is the entire point. Anyone approved can type any URL into our form, and until somebody proves otherwise the host on the end of it may be a company that has never heard of you: what we would be is a signed, authenticated traffic source aimed at somebody who never asked, with your name on every request. It is also why a DNS record on the domain or a file at some other path is not what we ask for. A tracking domain can have one operator per path, and the path we would be posting to is the one that is yours to prove.
- Give us the postback URL you want. We record it as nominated rather than live, and hand you a 64-character verification token with it. Nothing is sent to the URL until the rest of this is done.
- Make that exact URL answer a GET carrying flirtly_verify=1 with the token as the whole response body. Not JSON, not a page with the token in it, not a redirect to somewhere the token lives — the body, and nothing else. A trailing newline is fine. We read at most 256 bytes and stop.
- Tell us it is ready. We fetch the URL once. A 2xx whose body is the token puts it live; anything else comes back as what it was — the wrong body, a redirect (we never follow one), a status that was not 2xx, or no answer inside four seconds.
- Take the token back out afterwards if you like. It is single-use: ours is destroyed at the moment it is proved, so it verifies nothing a second time.
Changing the URL nominates the new one and takes the old one out of service, including changing it back to one that was live yesterday. A proof is about a URL, not about you, and we do not carry one across to a string that was typed later. If you lose the token before you have used it, ask for another — that leaves anything already live exactly as it is.
If your tracking platform will not let you control what one URL returns, that URL cannot be verified and we will not post to it. Point us at an endpoint on a host you do control and forward from there — from your servers, under your name, which is the difference this whole step is about.
Verifying a postback
Every postback carries a sig parameter: an HMAC-SHA256 over the rest of the query string, keyed with a secret that is yours alone. Your postback URL travels through ad networks, browser history and your own support tickets, so anyone who has seen it could otherwise write conversions into your reporting. Check the signature and they cannot.
- Parse the query string of the request you received into decoded name/value pairs, the way your framework already hands them to you, and drop the sig parameter. Everything else is signed — the fields we add and the ones that were already in your own postback URL. Work from the decoded pairs, not from the raw string: something between us may re-encode a character on the way, and re-encoding it yourself in the next step is what makes that harmless.
- Percent-encode every remaining name and value to RFC 3986: only A–Z, a–z, 0–9 and - . _ ~ survive, and every other byte of the UTF-8 becomes %XX in uppercase. That is PHP rawurlencode, Python quote(safe="-._~"), Ruby ERB::Util.url_encode. It is not http_build_query and not JavaScript encodeURIComponent — both differ on characters you will eventually see in a sub-ID.
- Write each pair as name=value, sort those strings byte-wise ascending, and join them with &. Sort them yourself rather than trusting the order they arrived in: your framework hands your handler a dictionary, and we make no promise about wire order.
- Prefix that with the literal v1 and one newline (0x0A). Everything above is now the string to sign.
- HMAC-SHA256 it with your secret, render the digest as lowercase hex, and compare it against sig with a constant-time comparison. The key is the 64-character secret exactly as we gave it to you — do not hex-decode it first. It looks like hex because it is easy to paste, not because it is meant to be turned back into 32 bytes.
Worked example — run your implementation against this before you go live
secret 6f1d2c3b4a5968778695a4b3c2d1e0ff00112233445566778899aabbccddeeff string v1\namount=12.50&event=commission&ref=goodsite&s1=fall%20promo&ts=1788160000&txn=t-42&uid=fe40c8c0efd61bdf91533fa264c59420 sig 6081e824191224317221323b766511889f4ff3230e906a0542aac6d636fde518
The \n in the middle string is one real newline byte (0x0A), written escaped here so you can see it. Everything after it is a single line.
ts is unix seconds and is inside the signature, so it cannot be edited without breaking it. A valid signature proves the request was ours, not that it is fresh — reject anything more than a few minutes off your own clock, and dedupe commissions on txn, which we send for exactly that.
Your secret is shown once, at the moment it is generated. We cannot read it back and neither can any of our dashboards. Lost it, or think it leaked? Ask for a rotation: the next postback is signed with the new secret and the old one stops verifying at that moment.