# Postbacks

> Get the sale in your tracker or on your server as it happens – and again on approval, rejection and refund. With macros, a signature, retries and a log.

A postback is a URL in your tracker (e.g. Voluum, RedTrack, Binom or Keitaro) or on your own server that Affilyflow calls when something happens to a sale on your links. Your tracker can then optimise campaigns on real sales – and you see the same as in the dashboard without fetching anything.

- The sale is sent when it comes in – and again when it is **approved**, **rejected** or **refunded**.
- Every call is **signed**, and a call without a 2xx response is **retried** for up to 24 hours.
- Every call is in a **log** with status code and response, and you can resend it.
- We never send the customer's data, the order amount or the store's full order number – only the last characters.

## Create a postback

1. Go to **AI og API** (AI and API) in the affiliate dashboard and find **Postbacks**.
2. Press **Ny postback** (New postback) and paste the URL from your tracker with macros (see below).
3. Choose **GET** (what most trackers use) or **POST med JSON** (POST with JSON), and choose the events.
4. Copy the secret if your own server is going to check the signature. It is shown only once.
5. Press **Send test** and check that your tracker received it.

You can have up to 5 postbacks. The URL must start with `https://` or `http://` and may only use port 443, 80, 8080 or 8443.

## Your tracker's click ID

Your tracker has its own click ID. Pass it in the Affilyflow link as a sub-ID, and it comes back in the postback:

```text title="The link in your tracker"
https://app.affilyflow.com/go/YOUR_LINK?sid={clickid}
```

`{clickid}` is your tracker's own macro (its name differs from tracker to tracker). In the postback you then use `{sub_id}`:

```text title="Postback in Affilyflow"
https://your-tracker.com/postback?cid={sub_id}&payout={commission}&currency={currency}&status={status}&txid={sale_id}
```

The names of your tracker's parameters (`cid`, `payout`, `txid` …) are in the tracker's own guide. You have five sub-IDs (`sid`, `sid2` … `sid5`) – see [Links and sub-IDs](https://www.affilyflow.com/en/docs/affiliate/links-and-sub-ids).

## Macros

| Macro | Content |
|---|---|
| `{click_id}` | Affilyflow's click ID (`c_…`). |
| `{sub_id}` | Sub-ID 1 from the link (`?sid=`). |
| `{sub_id2}` … `{sub_id5}` | Sub-IDs 2–5 (`?sid2=` … `?sid5=`). `{sub2}` … `{sub5}` work too. |
| `{commission}` | Your commission after the event, with a full stop and two decimals (`75.92`). After a partial refund: the commission after the refund. For a rejected sale `0.00`. |
| `{currency}` | The currency of the commission (e.g. `DKK`). |
| `{status}` | `pending`, `approved` or `rejected`. |
| `{event}` | The event, e.g. `sale.approved`. |
| `{order_ref_masked}` | The last characters of the store's order number (e.g. `••82`) – to talk to the store about the same sale. |
| `{sale_id}` | The sale's ID at Affilyflow – the same for every event on the sale. |
| `{event_id}` | The event's ID (`evt_…`) – the same if the call is retried. |
| `{program_id}` | The programme's ID. |

Every value is URL-encoded. An empty field stays empty (e.g. `{sub_id3}` when the link has no third sub-ID). Other macros are rejected when you save.

## Events

| Event | Sent when |
|---|---|
| `sale.created` | The store reports a sale on your link (pending approval). |
| `sale.approved` | The sale is approved and counts towards your balance. |
| `sale.rejected` | The sale is rejected – e.g. returned, cancelled or breaking the programme rules. |
| `sale.refunded` | The sale is fully or partly refunded. `{commission}` is the commission after the refund. |
| `sale.updated` | The commission changed in another way, e.g. a refund that is reversed. |
| `click.created` | Every click on your links, about 2 minutes after the click. Optional – many calls. A click we filter out as a bot before then is not sent. |

A sale that is rejected as soon as it is recorded (e.g. for breaking the programme rules) arrives as `sale.created` with `{status}` = `rejected` – always check `{status}`. A sale through a personal discount code has no click, so the click macros are empty.

## GET or POST

- **GET:** the URL is called with the macros filled in. This is what trackers expect.
- **POST with JSON:** the macros in the URL are filled in too, and the body is the whole event:

```json title="POST · sale.approved"
{
  "id": "evt_6f1c2d0e8b9a4c3d9e7f1a2b3c4d5e6f",
  "type": "sale.approved",
  "created_at": "2026-11-12T10:14:03.512+00:00",
  "test": false,
  "data": {
    "sale": {
      "id": "0b6b2d8e-3f1a-4c55-9d2e-7a1b2c3d4e5f",
      "order_ref_masked": "••82",
      "status": "approved",
      "previous_status": "pending",
      "commission": 75.92,
      "currency": "DKK",
      "created_at": "2026-10-13T18:22:41.087+00:00",
      "approved_at": "2026-11-12T10:14:03.512+00:00",
      "approval_due_at": "2026-11-12T18:22:41.087+00:00",
      "rejection_reason": null,
      "attribution": "click",
      "recurring": false,
      "refund": null
    },
    "click": {
      "click_id": "c_8Kq2mXo1Tz9w",
      "clicked_at": "2026-10-13T17:58:02.114+00:00",
      "link_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "sub_id": "fb-kampagne-12",
      "sub_id2": "annonce-3",
      "sub_id3": null,
      "sub_id4": null,
      "sub_id5": null,
      "landing_path": "/produkter/uldplaid",
      "device": "mobile",
      "country": "DK"
    },
    "program": { "id": "5d1e2f3a-4b5c-4d6e-8f70-819a2b3c4d5e", "store": "Nordlys Bolig" }
  }
}
```

On a refund, `refund` holds `type` (`partial` or `full`), `refunded_ratio`, `commission_before_refund` and `refunded_at`. For a rejected sale, `commission` shows the commission the sale had – it is not paid out (the `{commission}` macro is `0.00`). The store's rejection reasons in `rejection_reason` are written in Danish.

## The signature

Every call has the header `Affilyflow-Signature: t=…,v1=…`. `v1` is an HMAC-SHA256 with your secret over `t`, a full stop and:

- for **POST**: the raw body,
- for **GET**: the whole URL as it was called (with the macros filled in).

Trackers ignore the header, and that is fine. On your own server you should check it and reject calls where `t` is more than 5 minutes from your clock. Right after a new secret there can be two `v1=` (new and old) – accept the call if either matches. Code examples in Node.js and PHP are under [Webhooks](https://www.affilyflow.com/en/docs/store/webhooks#verify-the-signature) – it is the same signature.

## Retries, pausing and the log

- Respond with a **2xx** status within 10 seconds. Anything else is retried after about 1, 5, 15 and 30 minutes and then after 1, 2, 4, 6 and 8 hours – for up to 24 hours.
- After **20 failed attempts in a row** the postback is paused, and the dashboard says so. Fix the problem, send a test and press **Slå til** (Turn on). Calls from the last 24 hours are then sent.
- **Log** shows the latest 20 calls: event, order, status, status code, attempts and the first 500 characters of the response. **Send igen** (Resend) sends a call again. The log is kept for 30 days.
- **Send test** sends example data (`c_test0000000000`, sub-ID `test`, commission `39.92`) and shows the response straight away. The POST body has `"test": true`.
- The same event can arrive more than once. Use `{event_id}` (or `{sale_id}` + `{status}`) to ignore duplicates.

## Postbacks or the API?

Postbacks come to you when something happens. To fetch sales, clicks and numbers yourself, use the [REST API](https://www.affilyflow.com/en/docs/affiliate/rest-api) or the [MCP server](https://www.affilyflow.com/en/docs/affiliate/mcp).

Source: https://www.affilyflow.com/en/docs/affiliate/postbacks
