---
url: https://docs.dataflair.ai/stats/postbacks/contract.md
description: >-
  Send one conversion to DataFlair Stats with a GET or POST. Endpoint,
  authentication, fields, responses, conversion types and rate limit.
---

# Postback contract

A postback tells DataFlair Stats that one player did something: registered, made a first deposit, or made another deposit. You send one request for each event, from your server. DataFlair records it against the affiliate and the program.

## Endpoint

```text
GET or POST https://{stats-host}/api/postback/{program}/{token}
```

Copy the full URL from the Stats app: open the program, go to **Integrations**, and find the **Postback endpoint** card. The card also shows the masked token and has the **Generate token** and **Regenerate token** buttons.

Both methods work. A POST sends a JSON body. A GET sends the same fields as query parameters, which suits operators that fire a URL pixel. DataFlair merges the query string and the body, so the field names are the same either way.

Send `Accept: application/json` on every request so errors come back as JSON.

## Authentication

The `{token}` in the URL authenticates the request. Each program has its own token. It is 64 hexadecimal characters and is separate from the API key DataFlair uses to pull your reports.

* Treat the full URL as a secret. Send postbacks from your server. Do not put the URL in client-side code.
* If the token is wrong or missing, DataFlair answers `401`.
* **Regenerate token** in the app makes the old token stop working at once. Update your integration with the new URL.

### Signed requests (optional)

A POST can carry a signature in place of the token in the URL. Use the URL without the token segment, `/api/postback/{program}`, and add two headers:

| Header | Value |
| --- | --- |
| `X-Postback-Timestamp` | The current Unix time in seconds, as digits. |
| `X-Postback-Signature` | The lowercase hexadecimal HMAC-SHA256 of `{timestamp}.{raw request body}`, with your postback token as the key. |

DataFlair rejects a timestamp more than 300 seconds away from its own clock. Signing is for POST only. A GET postback uses the token in the URL.

## What you send

::: code-group

```bash [curl (POST)]
curl -X POST "https://{stats-host}/api/postback/12/YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "affiliate_id": "AFF-00042",
    "conversion_type": "ftd",
    "postback_id": "pb_001",
    "amount": 25.00,
    "status": "approved"
  }'
```

```bash [curl (GET)]
curl -G "https://{stats-host}/api/postback/12/YOUR_TOKEN" \
  -H "Accept: application/json" \
  --data-urlencode 'affiliate_id=AFF-00042' \
  --data-urlencode 'conversion_type=ftd' \
  --data-urlencode 'postback_id=pb_001' \
  --data-urlencode 'amount=25.00' \
  --data-urlencode 'status=approved'
```

:::

The [request builder](/stats/postbacks/request-builder) fills in these commands for you. It does not send them.

## Fields

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `affiliate_id` | string | yes | The DataFlair affiliate ID, for example `AFF-00042`. The affiliate must be approved for the program. `aff_id` is accepted in its place when the program has no field mapping. |
| `conversion_type` | string | yes | One of `registration`, `ftd`, `deposit`. See [Conversion types](#conversion-types). |
| `postback_id` | string | yes | Your unique id for this event. Up to 255 characters. See [Idempotency](/stats/postbacks/idempotency). |
| `amount` | number | no | The amount, zero or more. Defaults to `0`. |
| `currency` | string | no | A currency code, up to 8 characters. DataFlair stores it as you send it. |
| `player_id` | string | no | Your internal player reference. Up to 255 characters. |
| `occurred_at` | datetime | no | When the event happened in your system. DataFlair reports the conversion on this date when it is present. Without it, DataFlair uses the time it received the request. |
| `sub_id` | string | no | The `sub_id` you received on the landing page. Up to 100 characters. Letters, digits, `_` and `-` only. See [Tracking links](/stats/tracking/). |
| `tracking_id` | string | no | The `tracking_id` you received on the landing page, in the form `TL-` followed by letters or digits. |
| `status` | string | no | One of `approved`, `pending`, `rejected`, `hold`, `adjusted`. Defaults to `approved`. See [Status values](/stats/postbacks/status-semantics). |
| `kyc_status` | string | no | The player's KYC state: `pending`, `verified` or `rejected`. |
| `kyc_verified` | boolean | no | A shorter form of `kyc_status`. `true` means verified and `false` means pending. `kyc_status` wins when you send both. |
| `df_click_id` | string | no | A DataFlair click id in the form `dfc_` followed by 16 hexadecimal characters. Send it back as `sub_id` instead. |

DataFlair ignores any other field. It records the source IP address of the request itself.

## What you get back

A new conversion:

```json
{
  "success": true,
  "conversion_id": 42,
  "commission_id": 17
}
```

`commission_id` is `null` when no commission record was created.

A repeat of a `postback_id` DataFlair already has:

```json
{
  "success": true,
  "duplicate": true,
  "conversion_id": 42,
  "commission_id": 17,
  "message": "Postback already processed (idempotent)."
}
```

## Errors

Every error body has `"success": false` and an `error` code. A `message` describes it.

| Status | `error` | What caused it | What to do |
| --- | --- | --- | --- |
| `401` | `unauthorized` | The token is wrong, or the URL has no token and no valid signature. The message says which: `Invalid postback token.`, `No postback token in URL. Expected: /api/postback/{program}/{token}`, or `Invalid or expired postback signature.` | Check the URL. Check that the token was not regenerated. |
| `404` | `program_not_found` | The program does not exist or is not active. | Check the program id. Ask the operator to activate the program. |
| `422` | `validation_error` | A field is missing or invalid. The `errors` object lists each field. | Fix the fields named in `errors`. |
| `422` | `affiliate_not_found` | The affiliate is not approved for this program. | Send the `affiliate_id` you received on the landing page. |
| `422` | `tracking_link_not_found` | The `tracking_id` is not valid for this program or affiliate. | Send the `tracking_id` you received, or leave it out. |
| `429` | none | You sent more than 60 requests in one second with the same token. The response has a `Retry-After` header. | Wait, then retry with the same `postback_id`. |
| `429` | none | The same `postback_id` arrived twice at the same moment. The message is `Concurrent delivery of the same postback_id; retry shortly.` | Retry with the same `postback_id`. |
| `5xx` | none | A server error. | Retry with the same `postback_id`. |

A `validation_error` looks like this:

```json
{
  "success": false,
  "error": "validation_error",
  "message": "Request validation failed.",
  "errors": {
    "conversion_type": ["The selected conversion type is invalid."]
  }
}
```

## Conversion types {#conversion-types}

| Type | Meaning |
| --- | --- |
| `registration` | A player account was created. |
| `ftd` | The player's first deposit. |
| `deposit` | A later deposit. |

Postbacks carry these acquisition events only. DataFlair does not accept `cashout` in a postback. Withdrawals and revenue reach DataFlair through the [Operator Reporting API](/stats/operator-api/contract), which DataFlair pulls.

## Timeouts and retries

The rate limit is 60 requests per second for each token. When the URL has no token, the limit applies to each source IP address.

Retry a `5xx`, a `429` and a timeout with the same `postback_id`. Do not retry a `401`, a `404` or a `422` until you have fixed the cause. The [Idempotency](/stats/postbacks/idempotency) page explains why a retry is safe.

## Test this

Check the endpoint, fire a test postback and replay a logged one from the Stats app. See [Testing postbacks](/stats/postbacks/testing).
