Postbacks
Signed postbacks for each credit and reversal.
Send a test before traffic runs. If a delivery fails, Flight Recorder, our trace of every conversion, shows the status and the error.
A postback is a server-to-server request: when a conversion happens, we call a URL on your server so you can credit the user. What is a postback URL?
https://postback.pebble.example
Your server/afos
- verifier matches
- act_9f1c27e4 · completed is new
- Answered 200 OK
01Credit, credit, reverse
One action ID, from credit to reversal.
Harbor Blocks, three postbacks
Skip this animation Skip these steps- 01 Install
The install is credited
Harbor Blocks pays you once, at install:
payout=4.2,status=completed, under its ownaction_id.Pebble Rewards' postback log · u_48213- 2:09 PM completed
action_id=act_9f1c27e4 payout=4.2 user_payout=0 conversion_name=Install
Credited the install
Your earnings from u_48213 $4.20
-
- 02 Step
A later step rewards the player
Games paid per install pay you at install; later steps reward the player. Level 5 posts back with
payout=0anduser_payout=0.35, so you can pay your user.Pebble Rewards' postback log · u_48213- 2:09 PM completed
action_id=act_9f1c27e4 payout=4.2 user_payout=0 conversion_name=Install
Credited the install
- 2:31 PM completed
action_id=act_2b84d1c0 payout=0 user_payout=0.35 conversion_name=Reach level 5
Gave the player 350 pebbles
Your earnings from u_48213 $4.20
-
- 03 Reversal
The install is reversed
Same
action_id,status=reversed, the amount it takes back and areasonin plain words when we recorded one.Pebble Rewards' postback log · u_48213- 2:09 PM completed
action_id=act_9f1c27e4 payout=4.2 user_payout=0 conversion_name=Install
Credited the install
- 2:31 PM completed
action_id=act_2b84d1c0 payout=0 user_payout=0.35 conversion_name=Reach level 5
Gave the player 350 pebbles
- 3 days later reversed
action_id=act_9f1c27e4 payout=4.2 user_payout=0 conversion_name=Install reason=IP mismatch / VPN or proxy detected
Took back the install
Your earnings from u_48213 $0.00
-
- 04 Your server
Dedupe on action_id and status
Key on both. On
action_idalone the reversal looks like a repeat and gets dropped.Pebble Rewards' postback log · u_48213Key on action_id and status
act_9f1c27e4 · completednewact_2b84d1c0 · completednewact_9f1c27e4 · reversednew
All three handled. Earnings from u_48213: $0.00.
Key on action_id alone
act_9f1c27e4newact_2b84d1c0newact_9f1c27e4seen, dropped
The reversal is lost. Your books still say $4.20.
Step 1 of 4
02Why we sign
Anyone who finds your URL could create rewards.
The Signature. A code made with your postback secret and sent with each postback, so your server can check it came from us.What is a signature? stops that. Every postback carries verifier, an HMAC-SHA256 of the full URL keyed with your ps_ secret. Without a secret we don't send, and your Postbacks page says so.
- 01
Take the full URL your server received and remove
verifier. - 02
Compute HMAC-SHA256 of the rest with your secret, as hex.
- 03
Compare in constant time. Credit on a match; reject anything else.
Runs in your browser. Nothing is sent.
import crypto from "node:crypto";
// fullUrl: the URL as received, https included.
export function verifyPostback(fullUrl, secret) {
const url = new URL(fullUrl);
const received = url.searchParams.get("verifier") ?? "";
url.searchParams.delete("verifier");
const expected = crypto
.createHmac("sha256", secret)
.update(url.toString())
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
} import hashlib
import hmac
import re
# full_url: the URL as received, https included.
# verifier is always the last parameter: cut it off the end.
def verify_postback(full_url, secret):
m = re.search(r"[?&]verifier=([0-9a-f]{64})$", full_url)
if not m:
return False
signed = full_url[: m.start()].encode()
key = secret.encode()
expected = hmac.new(key, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, m.group(1)) Networks: postbacks are signed once you create a secret.
In the docs
03Macros
Every value you can receive.
Put Macro. A placeholder in your postback URL, such as {sub_id}, that we replace with the real value.What is a macro? anywhere in your postback URL and we fill them in. We also add the standard parameters, then sign the whole URL.
What we sendyour macros we add these
https://postback.pebble.example/afos?user=u_48213&txn=act_9f1c27e4&amount=4.2&action_id=act_9f1c27e4&sub_id=u_48213&offer_id=aff_7c1e40b2d9a3&status=completed&payout=4.2&user_payout=0&offer_name=Harbor+Blocks&clicktimestamp=2026-10-29T19%3A06%3A00Z&conversion_name=Install&goal_id=1001&offer_id_numeric=136469376326051&goal_id_numeric=140574789535191&is_offer_complete=false&sub1=home_tab&sub2=exp_42&verifier=16de60dc2fc2ff3e68aff478b6b73738a3e266064a7ab9c32967f30f76fcfec1
Runs in your browser. Nothing is sent. Signed with the example secret.
| Macro | What it is | Example |
|---|---|---|
| The user ID you put on the tracking link. | u_48213 | |
| The conversion's action_id. A reversal repeats it. | act_9f1c27e4 | |
| What you earn for this step, in USD. On the Hosted Offerwall, the player's reward. | 4.2 | |
| Your earnings. Same as payout, except on the Hosted Offerwall. | 4.2 | |
| The player's reward for this step. Empty when none is set. | 0.35 | |
| Our offer ID, the off_id on your link. | aff_7c1e40b2d9a3 | |
| The step's ID. Empty on an offer with no steps. | 1001 | |
| The offer ID as a whole number, up to 15 digits. Store it as a 64-bit integer. | 136469376326051 | |
| The step's ID as a whole number, up to 15 digits, unique across offers. Store it as a 64-bit integer. | 140574789535191 | |
| The step's name, not the offer's title. | Install | |
| The offer's title. Store it: a pulled offer can't be looked up later. | Harbor Blocks | |
| Either completed or reversed. | completed | |
| Why a reversal happened, when we recorded why. Empty on credits. | IP mismatch / VPN or proxy detected | |
| One of purchase, deposit or subscription, when the step is one. Empty otherwise. Macro only, never appended. | purchase | |
| The step's price in USD as a plain number, when its name states exactly one price. Empty otherwise. Macro only. | 9.99 | |
| Your own values from the tracking link. Empty when the link carried none. | home_tab |
Matching ignores case, so {SUB_ID} works too. A macro with no value becomes empty, never braces. Anything else in braces stays in the URL, encoded: {nope} arrives as %7Bnope%7D.
In the docs
04When something goes wrong
When a delivery fails, you see why.
Flight Recorder traces every conversion from the tap to your server and records each delivery with its result.
Conversion trace
act_9f1c27e4
Timeline
- Click observed ok
A matching click was found for this offer and sub ID.
10/29/2026, 2:06:00 PM
- Conversion received ok
Conversion recorded for Install; status is completed.
10/29/2026, 2:09:00 PM
- Payout calculated ok
Your payout is $4.20.
10/29/2026, 2:09:00 PM
- Postback delivery failed
connect ECONNREFUSED 203.0.113.5:443
10/29/2026, 2:09:01 PM
In the docs
Promises
Three rules our sender keeps.
- 01
We never send a publisher an unsigned postback.
No secret means no send, and your Postbacks page tells you.
- 02
Every reversal repeats the action ID it takes back.
With
status=reversedand the amount, so your books can match it. - 03
Every delivery is on record.
Its status, your server's HTTP answer and the error, in Flight Recorder.
Questions
What developers ask about postbacks.
GET or POST?
GET. Any 2xx answer counts as delivered; we recommend 200.
Do postbacks arrive in order?
Don't rely on it. We send a reversal only after its credit was delivered, but key on action_id and status together rather than on arrival order.
What happens when my server is down?
We send up to 3 times, all within 20 seconds. A refused connection, a 5xx or a 429 is tried again after a short pause; a server that hasn't answered by the time limit, or any other 4xx, is not. Then the delivery is recorded as failed, with the error, in Flight Recorder. Outside the Hosted Offerwall, our team can resend a failed credit under its original action_id.
What happens when I rotate my secret?
The new secret takes effect at once and the old one stops working, so rotate when you can update your server straight away.
How do I test before sending traffic?
Use Send test postback on your Postbacks page. It goes through the same macros, parameters and signing as a real conversion, with sample values: sub_id=test_user_123, payout=1, an action_id starting test_ and test=true. It carries none of your sub1 to sub10, since no click is behind it. No conversion or balance is created.
I run a network. Are my postbacks signed?
Once you create a secret. Until then, network postbacks go out unsigned, so create one before you send traffic.
Send a test before your first click.
Sign-up is by request. We reply by email.