Postbacks, Pixels and Image Tags: Only One of Them Can Be Paid Against
Three mechanisms for reporting a conversion, described so a non-engineer can hold them, plus the six ways integrations actually break. Written for operators and technical affiliate managers who have debugged this at eleven at night.

A conversion can be reported three ways: an image tag, a JavaScript pixel, or a server-to-server postback. Only the postback holds up well enough to pay commission against, because it does not need the player's browser to cooperate. Most integrations that "don't track" are not broken postbacks at all, they are missing click identifiers on the player record.
The call always comes at a bad hour. An affiliate manager has a partner threatening to pull traffic, the operator swears the integration is live, and the dashboard says zero. Somebody sends a screenshot of a tag in the footer of the deposit-success page and asks why it isn't working.
It isn't working because it is a tag in the footer of a page.
Reporting a conversion is a small problem with a lot of ways to get it wrong, and the ways have not changed much in ten years. What changed is that two of the three mechanisms quietly stopped being reliable while everybody kept shipping them anyway. This piece is the one to hand your engineer before the integration, not after.
Three ways to say that a player deposited
The image tag. The oldest one. Your page includes a one-pixel image whose source is a URL belonging to the tracking platform. The browser tries to load the image, that request hits the platform, and the request itself is the message. No image is really needed, the fetch is the point. It requires nothing but HTML, which is why it survived so long in template systems where nobody wanted to touch JavaScript.
The JavaScript pixel. Same idea with a script instead of an image. A snippet runs in the page, collects a little context, and sends the call. It is more flexible: it can read a value the page put in a variable, it can wait for a confirmation to render, it can pass a deposit amount that was only known at runtime. It is also more fragile, because it depends on the script loading, the variable existing, and the page not having thrown an error above it.
The server-to-server postback. No browser at all. When your backend records the event that matters, it makes an HTTP request to a URL, carrying the identifier it was handed when the player first arrived. Your payments service confirms a deposit, your system writes the transaction, and somewhere in that same path a request goes out saying: this player, this event, this amount, this currency.
That is the whole idea. It sounds less clever than the other two, and that is precisely the reason it holds up.
Why two of the three stopped being dependable
There is no single cause. There is a stack of them, and each one on its own would be survivable.
Safari and Firefox block third-party cookies by default, and Firefox also blocks requests to domains on its tracker list. Chrome still allows third-party cookies, so on Chrome the blocking comes from the ad blocker rather than the browser. Safari additionally caps how long script-written storage survives, in some cases down to days, which means anything relying on state written at click time and read at deposit time has an expiry measured in a fraction of a normal consideration window. Ad blockers sit on top of all of that and are common in exactly the audiences who read comparison content before signing up. None of it announces itself. Your operator sees the page render fine and concludes the integration works.
Then there is the part no browser setting can fix. A player clicks on a desktop at work, thinks about it, and deposits that evening from the phone. Or registers on the web and deposits inside the native app, where there is no browser, no page, and no tag to fire. In sportsbook especially the deposit follows the fixture list rather than the click, and it follows it into the app. If you want to see how differently those two journeys behave, casino and sportsbook traffic diverge on exactly this point.
A pixel cannot fire from a place the browser never visits. Nothing about that is fixable with better tag placement.
| What you care about | Image tag | JavaScript pixel | Server-to-server postback |
|---|---|---|---|
| Fires from | The player's browser | The player's browser | Your own backend |
| Survives ad blockers | Rarely | Rarely | Yes, not involved |
| Survives cross-device | No | No | Yes |
| Survives an in-app deposit | No | No | Yes |
| Can read page state at runtime | No | Yes | Not applicable |
| Reports a settled amount | Only what the page knows | Only what the page knows | Yes, from the system of record |
| When to use it | Legacy systems you cannot change | A same-session signal, as a secondary | Anything that decides money |

The failure modes worth knowing
Nobody breaks an integration in an interesting way. The same six mistakes account for nearly all of it.
Firing on page load instead of on the event. The tag lives on the deposit-confirmation page, so it fires every time that page is opened. A player refreshes, comes back through history, or lands there after a failed attempt, and each one is a conversion in your reporting. This inflates numbers in the direction that looks good, which is why it survives so long before anyone questions it.
Is it actually server-side? A backend framework rendering a template is still emitting a tag the browser has to execute. If the request originates from the player's device, it is browser-side, whatever the file it was written in. The test takes a minute: block scripts in your own browser and see whether the event still arrives.
Then there is the click identifier, missing from the player record. This is the most common failure by a wide distance and it gets its own section below.
The deposit gets reported before it settles. The payment is authorised, the postback goes out, the payment then fails or is reversed. Now there is a commission attached to money that never arrived. Fire on the state your finance team would recognise, not on the state your checkout page shows.
Idempotency, or the absence of it. Networks time out. A well-built sender retries, which is correct. If the receiving side cannot tell that the second attempt is the same event as the first, the retry becomes a second conversion. Every event needs a stable unique reference from your side, the same value on every retry, so a duplicate can be recognised and discarded rather than paid twice.
Events also arrive out of order. A deposit lands before the registration it belongs to, because two services fired independently and one was faster. Order the events that depend on each other, or make each one carry enough context to stand alone.
The one thing that decides everything
Every failure above is repairable in an afternoon. This one is not.
When a player arrives through a tracking link, the identifier on that link has to be stored on the player record at registration. Not in a session. Not in a cookie. On the row in your database that represents that account, alongside the email address, in the same transaction that creates it.
If that does not happen, nothing downstream can be attributed. The player deposits three weeks later, your backend fires a technically perfect postback, and it carries no identifier, because there is nothing to carry. No amount of postback tuning fixes it. You are not debugging a reporting problem, you are debugging a registration problem, and the fix is in the sign-up flow.
Two related habits save a lot of arguments. Keep the identifier even when the value looks strange, because you are storing a token, not validating one. And keep it for the life of the account rather than for thirty days, because a CPA that pays on a qualifying deposit needs the link to still exist at the moment the deposit qualifies, which can be months after the click. If the vocabulary around qualifying deposits and first-time deposits is fuzzy on your side, the glossary of FTD, NGR, CPA, CPL and CPR is worth ten minutes before you write the schema.
Testing it before traffic runs
A correct integration test is boring and takes about an hour.
- Click a real tracking link and complete registration as a normal player would. Then look at the player record in your own database and confirm the identifier is on it. If it is not there, stop, because everything after this is theatre.
- Deposit real money. A small amount is fine, EUR 20 will do. Test-mode payments frequently skip the exact code path that fires the event, which is how integrations pass in staging and fail on day one.
- Check that the deposit event arrived once, with the right amount and the right currency, after the payment settled rather than when it was submitted.
- Send the same event again deliberately. It should be recognised as a duplicate and not counted twice.
- Repeat the whole thing on a phone, and once more with the deposit made in the app rather than the browser if you have one.
An hour of an engineer's time and one deposit, against a month of reconciliation with an affiliate who is already convinced you are shaving their traffic. The tracking link QA walkthrough covers the affiliate side of the same test, and running both ends of it together is how you find the mismatch in an afternoon instead of a quarter.
What AFFILIFY expects from an operator
The integration reference lives at affilify.partners/docs, with the endpoint, the parameter names and the accepted event types. Read it there rather than from a blog post, because that page is the one we keep current.
The shape is small. An event name, the identifier you were handed at click time, an amount and a currency, plus your key. Registration, first-time deposit, subsequent deposits and revenue events are separate messages, because the commission model attached to a deal decides which of them matter, and a program cannot pay RevShare on data it never received. If you are still choosing between models, CPA, RevShare and hybrid sets out what each one needs from your side.
Two things worth saying plainly. Attribution on our side has layered fallbacks. They are insurance, not a plan. The identifier stored at registration is the part you control, so send it. And traffic quality is assessed continuously, which means some conversions are held while they are checked rather than credited on arrival. That is not a rejection, and the affiliate sees the state rather than a silent gap.
An operator who returns the identifier, reports settled events once, and can be tested with a real deposit in an hour will never have the eleven-at-night conversation. That is the entire ambition here.
Frequently asked questions
What is the difference between a postback and a pixel?
A pixel is fired by the player's browser while a page is open. A postback is fired by your own backend when the event is recorded, with no browser involved. That single difference decides everything else: the postback is not affected by ad blockers or by browser cookie policy, it still works when the player registers on a laptop and deposits from a phone, and it works when the deposit happens inside a native app where no page ever loads. It also reports the amount your system of record holds, not the amount a page happened to know.
Can we keep a pixel as a backup alongside the postback?
You can, but be clear about what it is worth. A pixel will only ever confirm events it can see, which is a subset of the events that actually happened, and the size of that subset is invisible to you. Treat it as a same-session sanity check during development rather than a second source of truth, and never reconcile payments against it. If both fire for the same event, your platform needs a stable unique reference from your side so the pair is recognised as one conversion and not two.
Our postbacks are firing but nothing gets attributed. Why?
Almost always because the click identifier was never stored on the player record at registration. The postback goes out correctly, on time, with a valid key, and carries nothing to match on. Check one real player row in your database first. If the identifier is missing there, the bug is in your sign-up flow, not in the postback code, and no amount of retry logic or endpoint tuning will change the outcome.
How should we test a postback integration before running traffic?
Click a real tracking link, register as a player, and confirm the identifier landed on the account row. Then deposit real money, because test-mode payments often skip the exact code path that fires the event. Confirm the deposit arrived once, with the right amount and currency, after the payment settled. Send the same event again on purpose and confirm it is discarded as a duplicate. Then repeat on a phone, and in the app if you have one. An hour, and one small deposit.
