---
url: https://docs.dataflair.ai/stats/operator-api/contract.md
description: >-
  The read-only daily aggregate endpoint you build so DataFlair Stats can pull
  registrations, first deposits and deposits for each affiliate.
---

# Operator Reporting API

You build one read-only endpoint on your server. It returns daily totals for each affiliate. DataFlair Stats calls it on a schedule and stores the numbers. Operators see every affiliate in the app. An affiliate sees only their own rows.

## What we send

DataFlair sends a `GET` with a date range and one credential header.

::: code-group

```http [HTTP]
GET /api/reports/affiliate-daily?from=2026-02-01&to=2026-02-07 HTTP/1.1
Host: operator.example.com
X-API-Key: YOUR_KEY
Accept: application/json
```

```bash [curl]
curl "https://operator.example.com/api/reports/affiliate-daily?from=2026-02-01&to=2026-02-07" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Accept: application/json"
```

:::

| Query parameter | Type | Required | Meaning |
| --- | --- | --- | --- |
| `from` | `YYYY-MM-DD` | yes | The first day, inclusive. A UTC date. |
| `to` | `YYYY-MM-DD` | yes | The last day, inclusive. A UTC date. |

DataFlair calls the URL you save on the program's **Integrations** page, exactly as saved. It removes any query string from that URL and adds `from` and `to`. The path `/api/reports/affiliate-daily` is the convention. A different path works when you save it.

If your API uses other names for `from` and `to`, or nests the rows in a different place, set a field mapping for the pull on the Integrations page. DataFlair reads the mapping for the parameter names and for the location of the `rows` array.

### Authentication

Use a read-only credential. DataFlair sends it in one of these headers, according to the type you chose when you saved the connection:

| Type | Header |
| --- | --- |
| API key (recommended, the default) | `X-API-Key: <key>` |
| Bearer token | `Authorization: Bearer <token>` |

## What you return

Status `200` with a JSON body:

::: code-group

```json \[exemplary/affiliate-daily/expected-response.json]
{
  "from": "2026-02-01",
  "to": "2026-02-07",
  "rows": [
    {
      "date": "2026-02-01",
      "affiliate_id": "AFF-00042",
      "registrations": 28,
      "ftds": 7,
      "deposit_amount": 840.00,
      "commission_amount": 100.80
    }
  ]
}

```

:::

`rows` may be empty. An empty array is a valid answer.

## Fields

The response:

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `rows` | array | yes | One object for each day and affiliate. |
| `from`, `to` | string | no | The range you answered for. DataFlair reads them when they are present. |

Each row:

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `date` | `YYYY-MM-DD` | yes | The reporting day of the row. |
| `affiliate_id` | string | yes | The affiliate ID from the tracking link, for example `AFF-00042`. Return it exactly as you captured it. |
| `registrations` | integer | one metric is required | Player accounts created. |
| `ftds` | integer | one metric is required | First-time depositors. |
| `deposit_amount` | number | one metric is required | The sum of deposits. |
| `commission_amount` | number | one metric is required | The commission you attribute to the affiliate for the day. Leave it out or send `null` when you do not calculate it. |
| `clicks` | integer | no | Clicks you track yourself. DataFlair tracks clicks on its own. |

At least one of `registrations`, `ftds`, `clicks`, `deposit_amount`, `commission_amount`, `reported_ngr` or `kyc_verified_ftds` must be on the first row of the response. Without one, DataFlair fails the whole pull as a schema mismatch. DataFlair also reads `ggr`, but it does not count toward that check. The [Metrics](/stats/operator-api/metrics) page explains each metric.

DataFlair skips a row that has no `date`, no `affiliate_id`, or a `date` that is not in the form `YYYY-MM-DD`. It stores the other rows.

The affiliate ID must be one DataFlair generated. You do not create affiliate IDs. You capture them from the landing URL, as described on [Tracking links](/stats/tracking/).

## Errors

| Status | What caused it | What DataFlair does |
| --- | --- | --- |
| `200` | Success. | Reads the rows and stores them. |
| `400`, and other `4xx` | A bad request, or a date it could not read. | Records the run as failed. Does not retry. |
| `401`, `403` | The credential is wrong or missing. | Records the run as failed. Does not retry. |
| `429` | Rate limited. | Waits for the `Retry-After` header, 10 seconds when the header is missing, at most 60 seconds. Then retries. |
| `5xx` | A server error. | Waits 2 seconds, then 4 seconds, and retries. |
| `200` with no `rows` array, no `date` or `affiliate_id` on the first row, or no metric on the first row | The body does not match the contract. | Records the run as failed with a schema mismatch. Does not retry. |

## Timeouts and retries

DataFlair waits up to 30 seconds for each request. It makes up to 3 attempts in one run. The waits are in the table above.

## How DataFlair pulls it

Each program picks a schedule on the **Integrations** page:

| Schedule | When DataFlair calls your endpoint |
| --- | --- |
| Daily | Once a day at 03:00 UTC. |
| Hourly | Every hour, on the hour. |
| Manual | Only when an operator clicks **Pull now**. |

Every pull asks for a range of days, not one day. The range and how late data is corrected are on [Timezone and backfill](/stats/operator-api/timezone-and-backfill).

## Test this

Save your endpoint and credential in the **Pull API** connection on the Integrations page. Click **Test connection**. DataFlair calls your endpoint for yesterday, and it reports what it received. Click **Pull now** to run a full pull.

## Verify

Before you save the connection, run the [conformance check](/stats/operator-api/conformance) against your endpoint. It replays the [fixtures](/stats/operator-api/fixtures) and links each failure to the section of this page that explains it.
