> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.offerzy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify postbacks

> Check the signature on every postback with your secret key, so only requests from Offerzy can credit your users.

Anyone can send a request to your postback URL. The `signature` parameter proves a postback came from us: only Offerzy and you know the secret key it's made with.

<Warning>
  Never credit a user without checking the signature. A forged postback could give away unlimited currency.
</Warning>

## How the signature is made

```text theme={null}
signature = md5(user_id + "|" + reward + "|" + offer_id + "|" + secret_key)
```

It's an MD5 hash in lowercase hexadecimal, made from three postback parameters and your secret key, joined with `|`.

<Steps>
  <Step title="Take the values exactly as received">
    Use `user_id`, `reward` and `offer_id` as the strings in the request. Don't convert `reward` to a number first: on a reversal it's negative, for example `-1500`, and formatting it differently changes the hash.
  </Step>

  <Step title="Join them with your secret key">
    For `user_id=user123`, `reward=1500`, `offer_id=offer_123` and the secret key `sk_ofz_example`, the string to hash is:

    ```text theme={null}
    user123|1500|offer_123|sk_ofz_example
    ```
  </Step>

  <Step title="Compare in constant time">
    Hash the string with MD5 and compare it with `signature` using a constant-time comparison: `crypto.timingSafeEqual` in Node.js, `hash_equals` in PHP, `hmac.compare_digest` in Python. If they differ, answer `403` and don't credit anything.
  </Step>
</Steps>

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from "node:crypto";

  function isFromOfferzy(query, secretKey) {
    const expected = crypto
      .createHash("md5")
      .update(`${query.user_id}|${query.reward}|${query.offer_id}|${secretKey}`)
      .digest("hex");
    const given = String(query.signature ?? "");
    return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  }
  ```

  ```php PHP theme={null}
  function isFromOfferzy(array $query, string $secretKey): bool {
      $expected = md5("{$query['user_id']}|{$query['reward']}|{$query['offer_id']}|{$secretKey}");
      return hash_equals($expected, (string) ($query['signature'] ?? ''));
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac


  def is_from_offerzy(query, secret_key):
      raw = f"{query.get('user_id', '')}|{query.get('reward', '')}|{query.get('offer_id', '')}|{secret_key}"
      expected = hashlib.md5(raw.encode()).hexdigest()
      return hmac.compare_digest(expected, query.get("signature", ""))
  ```
</CodeGroup>

## Your secret key

Find it in the dashboard under **Settings > Secret key**. It starts with `sk_ofz_`, and one key signs the postbacks of all your placements.

* Keep it on your server only, in an environment variable or a secrets manager. Never put it in your website's code or your app.
* If it leaks, click **Regenerate key**. The new key applies to all your placements at once and the old one stops working, so update your server right away. Postbacks your server rejects in the meantime are [retried](/postbacks/testing#retries).

## More good practice

* Use an `https://` postback URL.
* Credit users only from postbacks, never from anything the offerwall page or your app reports.
* Keep a record of every `transaction_id`, so you can match reversals and answer support questions.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.