How to answer a postback, timeouts and retries
Store the postback, answer with any 2xx status, then credit the user. We judge your answer by its status code alone: a 2xx ends the delivery, a timeout, a 5xx or a 429 gets a quick retry on publisher postbacks, and any other answer is final.
See also: Signed postbacks
On this page
| Your server’s answer | What we record | Do we try again? |
|---|---|---|
| Any 2xx, such as 200 or 204 | sent | No |
| A 5xx, such as 500 or 503 | failed, if every try fails | Yes, on publisher postbacks |
| 429 | failed, if every try fails | Yes, on publisher postbacks |
| Any other 4xx, such as 400, 401 or 404 | failed | No |
| No answer in time, or no connection at all | failed, with the error | Yes, on publisher postbacks |
We never judge the body of your answer. It may show under Response in Flight Recorder, but a 200 that says “error” in its body still counts as sent, and a sent delivery means your server answered, not that it credited anyone.
- Publisher postbacks get up to three tries. That covers the Offer API, the Top Offers Feed, Offer Links and the Hosted Offerwall. The second and third tries follow a fraction of a second after the one before.
- All the tries share one time limit of 20 seconds at most. A server that hangs on the first try uses it up, so a slow answer gets no second try. Answer within a few seconds.
- Network postbacks are sent once. There is no retry on a timeout, a 5xx or a 429.
- Nothing is resent later on its own. After the last try, the delivery stays failed.
Do the slow work after you answer. In order:
- Check the
verifier. If it doesn’t match, answer 401 and credit nothing. A 401 is final, so we don’t send it again. - Store the postback under its
action_idandstatustogether. If that pair is already stored, answer 200 and stop: it’s a repeat. - If storing fails, answer 500. On publisher postbacks we try again.
- Answer 200.
- Credit the user on
completed, deduct onreversed, from what you stored, in a queue or a background job.
Retries mean the same postback can reach you twice. If your server credits the user and then answers 500, we send it again. Storing first and checking the pair makes the second copy harmless. Dedupe on action_id alone and you drop the reversal of a conversion you already credited, because a reversal repeats the original action ID.
// Example receiver for Pebble Rewards (Express).
app.get("/afos", async (req, res) => {
if (!verified(req)) return res.sendStatus(401);
const { action_id, status } = req.query;
let isNew;
try {
// Unique on (action_id, status): a repeat inserts nothing.
isNew = await db.insertOnce(action_id, status, req.query);
} catch {
return res.sendStatus(500); // publisher postbacks are retried
}
res.sendStatus(200);
if (isNew) queue.push({ action_id, status });
});verified() is the signature check in Verify a signed postback with HMAC-SHA256. Point your postback URL at the endpoint that answers, not at a redirect.
Publishers: open Reporting, then Flight Recorder, and set Delivery to failed. Open a conversion: Your postback delivery shows when we tried (Attempted), the HTTP status your server returned, the Host, Path and Masked URL we called, and the Error when there was no answer. Copy request copies the URL and its parameters, with secrets masked.
Networks: Reporting, then Flight Recorder has the same Delivery filter. Open a postback and read Network Delivery Response: Attempted, Status, HTTP, Host, Path, Masked URL, Error and Response.
Two delivery results aren’t about your answer at all:
- failed with an error about a postback secret means nothing was sent: publisher postbacks are never sent unsigned. Create the secret under Postbacks.
- skipped_no_url means no postback URL matched the conversion. How to set up a postback URL covers which row takes which step.