Product
Publishers
Advertisers
Networks
Help
Pause animations
Browse help topics
How-to PublisherNetwork

Verify a signed postback with HMAC-SHA256

Updated

View as Markdown

Every postback we send a publisher carries a verifier: an HMAC-SHA256 of the full URL, keyed with your ps_ secret. Remove verifier, hash the rest of the URL exactly as you received it and compare. Credit nothing that fails.

See also: Signed postbacks

On this page

Before you start

  • Your postback secret. Publishers create it under Postbacks with Create postback secret; networks under Workspace settings with Generate Webhook Secret. It’s shown once.
  • Somewhere safe on your server to keep it, such as an environment variable. Never put it in an app or a web page.

Steps

  1. Take the URL exactly as it arrived

    Use the full URL of the request: scheme, host, path and query, in the order and encoding we sent. Don’t rebuild it from parsed parameters. Rebuilding drops parameters you didn’t expect and changes encoding, and the hash no longer matches.

  2. Remove the verifier

    Take the verifier parameter out and keep its value. Leave every other parameter where it is, including test and any you don’t use.

  3. Hash and compare

    Compute HMAC-SHA256 of that URL with your secret as the key, as a lowercase hex string. Compare it with the value you kept, using a constant-time comparison. On a mismatch, answer 401 and credit nothing.

    Node (Express):

    JavaScript
    const crypto = require("crypto");
    
    app.get("/afos", (req, res) => {
      const host = `https://${req.get("host")}`;
      const url = new URL(host + req.originalUrl);
      const got = url.searchParams.get("verifier") || "";
      url.searchParams.delete("verifier");
      const want = crypto
        .createHmac("sha256", process.env.AFOS_SECRET)
        .update(url.toString())
        .digest("hex");
      const ok =
        got.length === want.length &&
        crypto.timingSafeEqual(
          Buffer.from(got), Buffer.from(want));
      if (!ok) return res.status(401).send("Bad verifier");
      // Credit on completed, deduct on reversed.
      res.sendStatus(200);
    });

    Python:

    Python
    import hashlib, hmac, os
    
    def is_from_affiliateos(full_url: str) -> bool:
        base, _, query = full_url.partition("?")
        kept, got = [], ""
        for pair in query.split("&"):
            if pair.startswith("verifier="):
                got = pair[len("verifier="):]
            else:
                kept.append(pair)
        rest = "&".join(kept)
        unsigned = f"{base}?{rest}" if rest else base
        key = os.environ["AFOS_SECRET"].encode()
        want = hmac.new(
            key, unsigned.encode(), hashlib.sha256
        ).hexdigest()
        return hmac.compare_digest(want, got)

    The Python version works on the raw URL string, so nothing is decoded and encoded again on the way.

  4. Then dedupe and credit

    A verified postback can still be a repeat. Dedupe on action_id and status together: a reversal repeats the action_id of the conversion it takes back, so deduping on action_id alone drops the reversal.

  5. Check it worked

    Choose Send test postback on a saved row (networks: Test Postback). Your server should accept the verifier and see test=true. Then change one character of the secret in your config and send it again: the check must fail.

Things that trip people up

  • The scheme or host changed on the way in. Behind a load balancer or proxy, your framework can report http or an internal host. Hash the public URL we called: the URL in your postback row, with macros filled in and our parameters added. The Node sample above fixes the scheme to https for that reason.
  • A framework decoded the query. + and %20 hash differently. Read the raw query string.
  • A new parameter appeared. Every parameter we send is inside the signature, so verifying the URL as received keeps working when we add one. Checking against a fixed list of names breaks.
  • Parameters in your own URL. Your own parameters are part of the URL we sign, so they’re covered too. Keep them as they are in your row.

Rotating the secret

Publishers: Postbacks, then Rotate secret. Only an owner or admin sees it. The old secret stops working the moment the new one is created, so every postback after that fails your check until your server has the new value. Have the change ready to deploy, rotate, then paste the new secret in straight away.

Deliveries your server refused in between show as failed in Flight Recorder. Send us their action IDs and we can send the credits again, except Hosted Offerwall ones.

Networks

Network postbacks carry a verifier only while a Webhook Secret exists. Without one, they’re sent unsigned. Revoke removes the secret, and from then on postbacks arrive unsigned until you generate a new one, so generate the new secret straight after revoking.

Postback secret. A key starting with ps_ that signs every postback we send you, so your server can check it came from us. Shown once.What is a postback secret?

Support

Still stuck? Write to us.

One address for every question: hello@affiliateos.ai. Put the details from this list in your first message so we can find the click or conversion without asking you for them again.

Put these in your message

  1. 01The email you sign in with
  2. 02Your role: publisher, advertiser or network
  3. 03The offer ID
  4. 04The click ID or action_id
  5. 05The time it happened, with your time zone
  6. 06What you expected to happen

Looking for a reward in an app? Ask that app's support. It credits your reward, so only it can see your balance.

Search help