# Postbacks

> One URL on your server receives every event: credits and reversals. Here is what we send and how to handle it.

Source: https://docs.sharklio.com/postbacks

## Setup

Add your postback URL in your dashboard under Apps, your app, Postback. We send a `GET` request to it for every event of every transaction, and we fill in the macros you put in the URL:

```
https://yoursite.com/postback?user={user_id}&tx={transaction_id}&event={event_id}&status={status}&reward={reward}&payout={payout}&hash={hash}
```

Use the macros you need, in any order and with any parameter names. Values are URL-encoded. The URL must use http or https on port 80, 443, 8080, or 8443, and point to a public server.

## Macros

| Macro | Description |
| --- | --- |
| `{user_id}` | The user ID you put in the offerwall link. |
| `{transaction_id}` | Stays the same for the whole life of a transaction: the credit and a later reversal carry the same value. |
| `{event_id}` | Unique for every event we send. Useful for logging. |
| `{status}` | What happened, as a number: 1 or 2. See [Statuses](https://docs.sharklio.com/postbacks#statuses). |
| `{state}` | The same as `{status}` in words: `credited` or `reversed`. |
| `{reward}` | The reward in your currency, after your conversion rate, user split, and rounding. Never negative. |
| `{currency}` | The name of your currency, for example Coins. |
| `{payout}` | What you earn for the transaction, in US dollars. Never negative. |
| `{offer_id}` | The ID of the offer. |
| `{offer_name}` | The name of the offer as your user saw it. |
| `{country}` | Two-letter country code of the user. |
| `{device}` | `desktop`, `android`, or `ios`. |
| `{ip}` | The IP address the user started the offer from. |
| `{sub1}` to `{sub5}` | The sub IDs you added to the offerwall link, the Offers API request, or the SDK session, or empty. See [Track sources with sub IDs](https://docs.sharklio.com/offerwall-link#sub-ids). |
| `{integration}` | How the user reached the offer: `offerwall` (the offerwall, opened by link, iframe, or the floating button), `api` (Offers API), or `sdk` (the mobile SDKs for Android, iOS, Flutter, React Native, and Unity). |
| `{hash}` | Signature to check that the postback comes from us. See [Verify the hash](https://docs.sharklio.com/postbacks#verify). |

## Statuses

| Status | State | What to do |
| --- | --- | --- |
| `1` | credited | Add `{reward}` to the user. |
| `2` | reversed | Take `{reward}` back from the user, if you credited this transaction before. |

Rules that keep your balances right:

- Credit only on status 1, and only once per `transaction_id`.
- A reversal (status 2) always carries the `transaction_id` of the credit it cancels. If you never credited that transaction, ignore it.
- Amounts are never negative. The status tells you whether to add or take back.
- The same event can arrive twice, for example after a retry. Store `transaction_id` with the last status you processed and skip repeats.

## Verify the hash

`{hash}` is the HMAC-SHA256, as lowercase hex, of the transaction ID, user ID, reward, and status joined with colons, signed with your postback secret key from the Integration tab. Use the values exactly as they arrive.

```php
$expected = hash_hmac('sha256', $tx . ':' . $userId . ':' . $reward . ':' . $status, $postbackSecretKey);
if (!hash_equals($expected, $hash)) {
    http_response_code(403);
    exit;
}
```

```javascript
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', postbackSecretKey)
  .update(`${tx}:${userId}:${reward}:${status}`)
  .digest('hex');
const valid = hash.length === expected.length
  && crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
if (!valid) return res.sendStatus(403);
```

```python
import hmac, hashlib

expected = hmac.new(postback_secret_key.encode(), f"{tx}:{user_id}:{reward}:{status}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, hash_value):
    abort(403)
```

The postback secret key is different from the link hash salt.

## Our IP addresses

Postbacks come from these addresses. If your server only accepts requests from known addresses, allow both of them:

| Type | Address |
| --- | --- |
| IPv4 | `187.124.177.111` |
| IPv6 | `2a02:4780:79:782c::1` |

We connect over IPv4 whenever your host has one, and use IPv6 only for hosts without an IPv4 address. Every request has the user agent `Sharklio-Postback/1.0`. An IP check is an extra layer: always verify the hash as well. If these addresses ever change, we tell you in advance.

## Responses and retries

- Answer with any 2xx status code within 6 seconds. We do not read the response body and we do not follow redirects.
- If your server fails or times out, we try again up to 5 times, after about 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours.
- Events of one `transaction_id` arrive in order. A reversal is never sent before the credit it cancels.
- Every attempt, with the status code your server returned, is listed in your dashboard under Reports, Postbacks. If the last attempt fails too, we email you, at most once a day per app, and you can send the postback again from that report.
- To check your handler before going live, use Send a test postback in Integration status on the Integration tab. It sends a credit or a reversal for a user ID and payout you choose. A test always has a `transaction_id` that starts with `test_` and `offer_id` 0, so your handler can tell tests from real results.

## Example handler

A complete handler for the URL in [Setup](https://docs.sharklio.com/postbacks#setup). Replace the four helper functions with your own storage.

```php
$tx     = $_GET['tx'] ?? '';
$userId = $_GET['user'] ?? '';
$reward = $_GET['reward'] ?? '0';
$status = (int)($_GET['status'] ?? 0);
$expected = hash_hmac('sha256', $tx . ':' . $userId . ':' . $reward . ':' . $status, $postbackSecretKey);
if (!hash_equals($expected, $_GET['hash'] ?? '')) {
    http_response_code(403);
    exit;
}
$last = get_last_status($tx);
if ($status === 1 && $last !== 1 && $last !== 2) {
    add_to_balance($userId, $reward);
    save_status($tx, 1);
} elseif ($status === 2 && $last === 1) {
    subtract_from_balance($userId, $reward);
    save_status($tx, 2);
}
http_response_code(200);
```

```javascript
const crypto = require('crypto');
const express = require('express');
const app = express();

app.get('/postback', async (req, res) => {
  const { tx = '', user = '', reward = '0', status = '', hash = '' } = req.query;
  const expected = crypto.createHmac('sha256', process.env.SHARKLIO_POSTBACK_KEY)
    .update(`${tx}:${user}:${reward}:${status}`)
    .digest('hex');
  if (hash.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected))) {
    return res.sendStatus(403);
  }
  const s = Number(status);
  const last = await getLastStatus(tx);
  if (s === 1 && last !== 1 && last !== 2) {
    await addToBalance(user, reward);
    await saveStatus(tx, 1);
  } else if (s === 2 && last === 1) {
    await subtractFromBalance(user, reward);
    await saveStatus(tx, 2);
  }
  res.sendStatus(200);
});
```

```python
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.get("/postback")
def postback():
    tx = request.args.get("tx", "")
    user = request.args.get("user", "")
    reward = request.args.get("reward", "0")
    status = request.args.get("status", "")
    expected = hmac.new(os.environ["SHARKLIO_POSTBACK_KEY"].encode(), f"{tx}:{user}:{reward}:{status}".encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.args.get("hash", "")):
        abort(403)
    s = int(status or 0)
    last = get_last_status(tx)
    if s == 1 and last not in (1, 2):
        add_to_balance(user, reward)
        save_status(tx, 1)
    elif s == 2 and last == 1:
        subtract_from_balance(user, reward)
        save_status(tx, 2)
    return "OK", 200
```
