Conversions
What happens between an advertiser saying "this user did the thing" and a publisher being able to withdraw the money.
States
| State | Means | Money |
|---|---|---|
| pending | Recorded and inside its hold period | Advertiser already debited; publisher amount is pending, not withdrawable |
| approved | The hold elapsed with no objection | Publisher balance credited and withdrawable |
| reversed | Withdrawn by the advertiser or by staff | Advertiser refunded; publisher amount removed |
| rejected | Never accepted — expired, capped, or fraud-scored out | Nothing moved at all |
The hold period
Each offer sets hold_hours, 72 by default. During it the advertiser can reverse the conversion and the publisher
cannot withdraw the money. When it elapses the cron worker approves everything
nobody touched.
The hold exists for one reason: without it a publisher can convert fraudulently, withdraw, and be gone before the advertiser has finished reconciling. It is the single most important control on the platform.
Deduplication
Two independent guards, so a retrying advertiser never pays twice:
-
A unique index on
(offer_id, external_txn_id)— the primary guard, used whenever the advertiser sends atxn_id. -
A unique index on
(click_id, goal_key)— the fallback when they do not. Safer, but it means one click can only ever convert a given goal once.
A duplicate returns 200 DUPLICATE, never an error.
Why one was rejected
Every inbound call is logged in full — query string, body, IP and outcome — under Postbacks on the advertiser dashboard. The usual causes, in order of how often they actually happen:
| Outcome | Usual cause |
|---|---|
UNKNOWN_CLICK | {click_id} truncated, re-encoded or dropped by a redirect chain |
INVALID_SIGNATURE | Fields concatenated in a different order, or the secret was rotated |
EXPIRED | Genuinely late, or the offer's window is shorter than your attribution window |
CAPPED | Daily cap, total cap, per-user cap, or the wallet cannot cover it |
DUPLICATE | Working as intended — your retry logic is doing its job |
Fraud scoring
Every conversion is scored before it is accepted. A score at or above the platform threshold is rejected outright; anything lower is recorded as a flag with a reason and left for review. The rules that run:
| Rule | Catches |
|---|---|
| Click-to-conversion floor | Conversions arriving seconds after the click |
| Velocity limit | An app suddenly producing far more conversions per hour than usual |
| Per-user cap | One user farming one offer |
| Duplicate IP | Many "different" users behind one address |
| Blocklist | IPs, countries, devices and users already known to be bad |
Reversals
An advertiser can reverse from their conversion list, or by calling the inbound
endpoint with status=reversed and the original txn_id.
Either way:
- The advertiser's wallet is refunded in full.
- The publisher's pending or approved amount is removed.
- A
status=reversedpostback goes out so the publisher can deduct the coins they granted. - Both ledger rows are written — nothing is deleted, ever.