Your postback URL returned 200. The conversion can still be lost
Published 2026-10-16
A postback that returns 200 OK feels like proof. It is proof of one thing: the request arrived. It says nothing about whether the right click ID and the right payout arrived with it.
We sent 22 test postbacks. 12 of them lost or changed data: 4 of the 10 GET requests and 8 of the 10 POST requests sent through a redirect. The receiver answered 200 to every GET request, including the broken ones. The fixes are small. Finding the problem without a test is not.
A postback is a server-to-server call that reports a conversion. The network or advertiser fires a URL at the tracker, or the tracker fires one at the traffic source, with the click ID and the payout in the query string. Nobody watches it happen, so nobody sees it fail.
Test 1: what the receiver actually reads
Ten GET postbacks, sent with curl to an echo server that returns the parameters exactly as it parsed them. Every one came back 200 OK.
| The URL carried | The receiver read | Result |
|---|---|---|
?clickid=abc123&payout=12.50 | clickid abc123, payout 12.50 | Correct |
?clickid={clickid}&payout={payout} | clickid {clickid}, payout {payout} | Macro never replaced |
?clickid=abc123&sub1=Tom&Jerry&payout=12.50 | sub1 Tom, plus a new empty parameter Jerry | Value cut at the & |
?clickid=abc123&sub1=Tom%26Jerry&payout=12.50 | sub1 Tom&Jerry | Correct |
?clickid=Ab+9x&payout=12.50 | clickid Ab 9x | The + became a space |
?clickid=Ab%2B9x&payout=12.50 | clickid Ab+9x | Correct |
?clickid=abc#123&payout=12.50 | clickid abc, no payout at all | Everything after # was never sent |
?clickid=abc%23123&payout=12.50 | clickid abc#123, payout 12.50 | Correct |
?clickid=abc123&payout=1,250.00 | payout 1,250.00, as text | Arrives, but a number field may refuse it |
?clickid=abc123&clickid=def456 | clickid as a list: abc123, def456 | Two values. The receiver’s code picks one |
Every failure follows the URL rules. &, +, = and # are reserved characters, and data that
contains them must be percent-encoded (RFC 3986).
# ends the query, so a client never sends what follows it. And form-style parsing turns + into a
space, so a click ID with a + in it stops matching its click.
Postman Echo is one parser, and those three results are the specs, not its quirks. The duplicate
parameter is where receivers part ways. Postman Echo kept both values. A receiver written against the
browser’s URLSearchParams takes the first (WHATWG URL),
and PHP takes the last. We parsed ?clickid=abc123&clickid=def456 three ways on 2026-10-16: Node 22’s
URLSearchParams read abc123; Python 3.11’s parse_qsl into a dict and PHP 8.3’s parse_str read
def456. The same postback credits two different clicks on two receivers.
Test 2: what a redirect does to a POST postback
Some postbacks are POST requests with the data in the body. We sent one through each redirect code to a local receiver, with two common HTTP clients.
| Redirect | curl 8.7.1 | Python 3.14 urllib |
|---|---|---|
| 301 | Turned into GET. Body dropped | Turned into GET. Body dropped |
| 302 | Turned into GET. Body dropped | Turned into GET. Body dropped |
| 303 | Turned into GET. Body dropped | Turned into GET. Body dropped |
| 307 | POST, body kept | Refused to follow. Error |
| 308 | POST, body kept | Refused to follow. Error |
GET postbacks kept their query string through both a 301 and a 308.
This is allowed behaviour. The HTTP standard says a client “MAY change the request method from POST to
GET” after a 301 or 302 (RFC 9110). So the same
redirect delivers the conversion from one sender and loses it from another. The safe-looking codes are
not safe either: Python’s standard library does not follow a 307 or 308 on a POST at all. It raises
an error, and the conversion is never sent. One way to get there
without noticing: an http:// postback URL on a server that now redirects to https://.
The token syntax changes on every platform
The click ID token is easy to mistype. Eight platforms, five click ID tokens, two wrappers, each from the platform’s own help pages:
| Platform | Click ID token | Payout token | Wrapper |
|---|---|---|---|
| TUNE | {transaction_id} | {payout} | { } |
| Everflow | {transaction_id} | {payout_amount} | { } only |
| CAKE | #s2# (returns the value sent in s2) | #price# | # # |
| ClickBank | {tid} | {affiliate_earnings} | { } |
| Voluum | {clickid} (case sensitive) | {payout} | { } |
| RedTrack | {clickid} | {sum} | { } |
| Keitaro | {subid} | {revenue} | { } |
| Binom | {clickid} | {payout} | { } |
Voluum’s {clickid} goes in the offer URL. When Voluum sends a postback on to a traffic source, its
token for the click ID is {click.id}.
The vendors document the same mistakes: TUNE lists [transaction_id] instead of {transaction_id},
Voluum warns that {clickId} is not {clickid}, and Everflow lists swapping the parameter and the
macro. Any of them gives you the second row of Test 1: a literal token where the click ID should be.
Why the platform’s own test button is not enough
Most platforms have a test tool. Several say, in their own docs, that it does not test the real path:
- Keitaro: affiliate networks “do not send a real subid in test postbacks”.
- RedTrack: a traffic channel postback needs live clicks, “as test clicks have no ref id”.
- ClickBank: the Test button fires events filled with fake data, and orders with a test card do not trigger the postback at all.
And the status code is not the verdict. TUNE returns 200 “even if unsuccessful”, with the real
answer, success=true or success=false, in the response body. Everflow reads a 200 from a partner
as “partner received it; issue is on their end”. After 10 failed deliveries with zero successful fires,
spanning more than 72 hours, Everflow switches the postback off.
How to test a postback URL, step by step
- Copy the tokens from the receiver’s docs. Never type them. Check the wrapper (
{ }or# #) and the case. - Send it to an echo server first. Swap the receiver’s domain for a request inspector, keep the path and tokens, and fire one real conversion. You see the raw URL that left the sender, with the tokens filled in or not.
- Fire one by hand with awkward values. Put
+,&and#in a test click ID and a payout with a comma. If any of them arrives changed, the sender is not encoding values. - Run one real click through the live link. Take the click ID from the landing page URL or the click log, convert, and find that exact ID in the receiver’s conversion report.
- Read the response body and the log, not the status code. Look for
success=true, “Affiliate Pixel Fired: Yes”, or the conversion itself in the report. - Point the postback at the final URL. Use
https://, the exact host, and no redirect. If a redirect cannot be avoided, make it a 307 or 308, and test it with the sender’s real client. - Test again after any change to the domain, the protocol, the tracking template or the token.
The last check in step 4 is a click ID, and it raises one more question. Which click ID belongs in the postback? The ad platform’s and the tracker’s look alike, and there are two kinds of click ID that never swap.
Sources
- RFC 3986: if data for a URI component would conflict with a reserved character's purpose as a delimiter, the data must be percent-encoded. & = + and # are reserved characters. verified 2026-10-12
- RFC 3986: the query component starts at the first question mark and is terminated by a number sign (#). verified 2026-10-12
- The WHATWG URL Standard's application/x-www-form-urlencoded parser replaces any + in a name or value with a space. verified 2026-10-12
- The WHATWG URL Standard: URLSearchParams get(name) returns the value of the first name-value pair whose name is name. verified 2026-10-16
- RFC 9110: after a 301 or 302, a user agent MAY change the request method from POST to GET for historical reasons. A 307 MUST NOT change the method, and 307 and 308 were added to indicate method-preserving redirects. verified 2026-10-12
- TUNE returns a 200 OK HTTP response code if it attempts to process the postback, even if unsuccessful. The response body carries success=true or success=false. verified 2026-10-12
- TUNE lists common postback URL errors including a missing https:// protocol, incorrect macro syntax such as [transaction_id] instead of {transaction_id}, and unencoded special characters in parameter values. It calls click session lifespan expiration the most common cause of postback failures. verified 2026-10-12
- Everflow's partner postback test reads response code 200 as: the partner received it, and the issue is on their end. 4xx and 5xx mean the partner's endpoint is rejecting or erroring. verified 2026-10-12
- Everflow macros are always wrapped in curly brackets, such as {transaction_id}; Everflow lists swapping the parameter and the macro as a common mistake. verified 2026-10-12
- Everflow's {payout_amount} macro is the amount paid out to a partner for a conversion or event, in the offer currency. verified 2026-10-12
- Everflow automatically deactivates a partner postback after 10 failed delivery attempts with zero successful fires, when the span from first to latest failure exceeds 72 hours. verified 2026-10-12
- CAKE tokens are wrapped in a pound/hash symbol on either side, for example #s1# or #tid#. verified 2026-10-12
- CAKE's affiliate test flow asks you to confirm that Affiliate Pixel Fired: Yes is displayed after the test conversion. verified 2026-10-12
- ClickBank's postback macros include {tid} for the tracking ID and {affiliate_earnings} for the earnings, and its Test button fires test events filled with fake data. Orders placed with a ClickBank test credit card do not trigger the postback. verified 2026-10-12
- Voluum token names are case sensitive: {clickid} is not the same as {clickId}. verified 2026-10-12
- RedTrack: live clicks are needed for a traffic channel postback test, because test clicks have no ref id. verified 2026-10-12
- Keitaro: affiliate networks do not send a real subid in test postbacks, so test the setup with a conversion through the tracker campaign link. verified 2026-10-12
- Binom: add the {clickid} token to the offer URL, or the postback will not return to the tracker and there will be no conversions. verified 2026-10-12
- TUNE's postback macros include {transaction_id} for the click and {payout} for the offer payout to the partner. verified 2026-10-12
- CAKE's postback URL uses the #s2# token to return the value the affiliate passed in s2, and #price# for the affiliate payout. verified 2026-10-12
- RedTrack's dynamic parameters: {clickid} passes the click ID from and back to RedTrack, and {sum} is the offer payout. verified 2026-10-12
- Voluum's tracking tokens: {clickid} is the Voluum click ID value and {payout} is the offer payout value. {click.id} passes the click ID to a traffic source with a postback URL. verified 2026-10-12
- Keitaro's built-in S2S postback placeholders include {subid} and {revenue}, the conversion revenue. Values are encoded by default; an underscore before the name inserts the raw value. verified 2026-10-12
- Binom's offer URL tokens include {clickid} and {payout}. verified 2026-10-12