A postback is an HTTP request Preads sends to your server when a conversion is recorded, so your system can credit the end user without polling.
{payout} and {reward} do not mean the same thing for every
network. See Macro semantics before you use them to credit users.
End user clicks an offer in the Preads wall
|
v
Preads records the click (GET /api/track)
|
v
The offer network completes the conversion
|
v
The network calls Preads (ANY /api/postback)
|
v
Preads verifies the click, calculates rewards,
credits your balance, writes the conversion row
|
v
Preads calls YOUR postback URL (GET, once)
Your postback is a notification, not a confirmation. The conversion is recorded and your balance is credited before your postback is sent. If your endpoint is down, the conversion still counts — you simply miss the notification.
approved. While it is pending no credit is applied, and if it later becomes
rejected the publisher balance is decremented.
The postback URL is set per app, so each of your apps can notify a different endpoint.
http:// or https://.127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16,
169.254.0.0/16, and similar) are rejected. The literal host localhost is
the one exception, so it works in development.
failed with the message "Blocked: postback URL is not a public http(s) address".
{user_id} you already control.
Macros in your postback URL are replaced with conversion values. Place them anywhere in the query string.
| Macro | Value | Currency |
|---|---|---|
{user_id} |
The end user's ID — the userId you passed to the wall or to
/api/track |
— |
{transaction_id} |
Unique conversion ID. Use this as your idempotency key. | — |
{offer_id} |
The ID of the offer's network, not the offer. See the note below. | — |
{offer_name} |
Offer name, with the step name appended for multi-step offers | — |
{payout} |
Meaning depends on the network type. See Macro semantics. | USD |
{reward} |
Meaning depends on the network type. See Macro semantics. | USD or app-local |
{ip} |
IP address recorded on the original click | — |
{country} |
Two-letter country code detected on the click | — |
{device} |
Device type detected on the click | — |
{status} |
approved, rejected or pending |
— |
{aff_sub} – {aff_sub4} |
Your own sub-parameters, echoed back from the click | — |
{currency}, {website_id},
{payout_local}, {timestamp} and {app_id} are not supported.
If you paste one into your URL it is sent to your server literally, braces included — it
does not error and it does not expand.
Every macro except {status} is URL-encoded before substitution. All values are therefore safe
to read directly from $_GET / request.query without further decoding.
{offer_id} is a network ID
{offer_id} resolves to the offer's network ID. It is not the
offerId you passed to GET /api/track, and it is not unique per offer — every
offer from the same network shares it. If you reconcile conversions against offers in your own reporting,
use {offer_name} or the network ID knowingly; there is no macro that gives you the offer ID.
{payout} and {reward} vary by network typeThese two macros read the same two database columns for every conversion, but the platform writes those columns differently depending on how the conversion arrived:
| Conversion arrived via | {payout} |
{reward} |
|---|---|---|
| Offerwall (API network) | What the network paid, USD | Your share, USD |
| Partner wall (iframe network) | Your share, USD | The end user's reward, in your app's currency |
{reward}, the same
postback URL will hand you a USD amount for one conversion and a local-currency amount for the next. Do not
assume a single currency for this field.
There is no macro that returns the end user's reward consistently across both network
types. If you need a reliable per-conversion breakdown, reconcile against the conversion you received
(using {transaction_id}) rather than inferring it from the amount fields.
A postback URL for a rewards endpoint:
https://api.example.com/rewards?uid={user_id}&txn={transaction_id}&offer={offer_name}&amount={reward}&status={status}&camp={aff_sub}
A request Preads sends for an approved conversion:
GET /rewards?uid=user_8812&txn=clk_68f2a1b0c3d4e5f6_a1b2c3d4e5f60718&offer=Complete+a+signup&amount=1.2500&status=approved&camp=summer_promo
| Property | Value |
|---|---|
| Method | GET |
| Timeout | 10 seconds |
| Attempts | One. No retries, no queue. |
| Signature | None. The request carries no HMAC, token or shared secret. |
| Success | Any 2xx status. Anything else is recorded as failed. |
| On failure | The conversion is still counted and your balance is still credited. |
| Trigger |
Whenever the conversion is created and whenever its status is resolved — so the same
conversion can notify you twice, once as pending and again as
approved or rejected.
|
{transaction_id} to match a conversion you already expect, using it as an
idempotency key.{user_id} to match a user who exists in your system.{user_id} per period, and reconcile totals against your
dashboard rather than summing postbacks.postback_secret is generated and displayed, but it is not used to
sign anything. It cannot be used to verify a postback.
Every attempt is recorded. Open Publisher → Reports → Postbacks to see the resolved URL, the HTTP status code, the first 1000 characters of your response body, and the error message when one occurred. The page also reports total, successful, failed and success-rate figures.
Each app has a test tool: Publisher → Apps → your app → Postbacks. It lets
you send a test postback with a chosen user_id and amount, re-send a previous test, simulate a
network rejection, view the fully substituted URL before sending, and delete tests.
{payout} is the amount
you typed in and {reward} is that amount multiplied by your app's exchange rate. In
real traffic {payout} is the network payout and {reward} is your USD share.
Use the test tool to check delivery, parsing and logging — not to calibrate amounts.
Test conversions are stored separately and are excluded from every figure in your dashboard, so sending
tests will not distort your reports. Test postbacks carry {status}=approved unless you
explicitly reject them, and their {transaction_id} is prefixed TEST_ — reject
those in your own handler.
Outbound postbacks are sent for offerwall conversions only. The behaviour differs by source:
| Conversion source | Sends your postback? |
|---|---|
| Offerwall — API network offer | Yes |
| Partner wall — iframe network wall | Yes |
| Content Locker | No |
| Link Locker | No |
Locker conversions are recorded and appear in your reports and statistics, but they are written with no app association and no outbound postback is ever sent for them. If you run a locker, do not build a crediting flow on postbacks — use the locker's statistics page or poll its progress endpoint instead.
For completeness: the endpoint offer networks post to is
https://preads.hansaldev.com/api/postback (any HTTP method, rate-limited to 300 requests/minute per IP). It
is unauthenticated by design, because third-party networks cannot be expected to sign requests.
It resolves the click ID from any of these parameters:
click_id, subId, subid, aff_sub, aff_sub1,
s2, transaction_id, txn_id, tid
Responses:
| Response | HTTP | Meaning |
|---|---|---|
OK |
200 | Conversion recorded |
1 |
200 | Duplicate — already recorded for this click and status |
0 |
400 | No click ID could be resolved from the request |
0 |
404 | Click ID not found |
Error |
500 | Processing failed |
Any payout, amount or reward figure a network sends is ignored. The platform uses its own offer record, so a network cannot credit itself.
| Symptom | Cause and fix |
|---|---|
| No postback ever arrives |
Check Postbacks in the app first — the log shows the resolved URL and the
exact error. Common causes: postback_url is empty (Preads returns early with
"No postback URL configured" and sends nothing); the URL points at a private or
loopback address and was blocked by the outbound guard; or the conversion came from a
Content or Link Locker, which never post back.
|
| Log shows "Blocked: postback URL is not a public http(s) address" | The host resolves to a private, loopback or link-local address. Use a publicly reachable
HTTPS endpoint. localhost is allowed for development only. |
| Log shows a timeout | Your endpoint did not respond within 10 seconds. It is not retried — trigger a re-send from the test tool. |
| Log shows a non-2xx status | Your endpoint responded, but not with 2xx. Any 2xx counts as success. Redirects are followed and the final status is recorded. |
{user_id} is empty in your logs |
No userId was supplied when the wall was embedded or the click was tracked. See
Offerwall Iframe. |
| You receive the same conversion more than once | Expected. A conversion notifies once when created and again when its status resolves.
Deduplicate on {transaction_id}. |
{offer_id} is the same for different offers |
Correct behaviour — it is the network ID. Use {offer_name} instead. |
{currency} arrived as literal text |
There is no such macro. It is passed through verbatim. |
userId and the aff_sub sub-parameters that come back here.