---
url: https://docs.dataflair.ai/stats/operator-api/conformance.md
description: >-
  Replay the DataFlair fixtures against your Operator Reporting API endpoint and
  see which rule a failing answer breaks. It runs on your machine and needs no
  DataFlair account.
---

# Operator API conformance check

The conformance check replays the [fixtures](/stats/operator-api/fixtures) against your endpoint. It prints one line for each case. A failing line says what is wrong and links to the section of the [contract](/stats/operator-api/contract) that explains it.

It runs on your own machine. It does not need a DataFlair account, and it does not need a program. It sends read-only `GET` requests.

::: warning The checker is not published to npm yet
`npx` cannot fetch it yet. It is in the DataFlair docs repository, in `docs-site/tools/stats-check`. Run it from the `docs-site` folder, after `npm ci`. The fixtures work without it: send the `from` and `to` in each `request.json` with `curl` and compare the answer with `expected-response.json`.
:::

## Run it

```bash
node tools/stats-check/bin/stats-check.mjs https://operator.example.com/api/reports/affiliate-daily --key YOUR_KEY
```

Use the URL you save on the program's **Integrations** page. Use a test credential. A command line stays in your shell history, so you can set `STATS_CHECK_KEY` instead of `--key`.

| Option | Meaning |
| --- | --- |
| `--key <key>` | The credential to send. Or set `STATS_CHECK_KEY`. |
| `--auth <type>` | How to send it, as when you save the connection: `api-key` sends `X-API-Key`, and `bearer` sends `Authorization: Bearer`. The default is `api-key`. |
| `--docs <url>` | Where the failure links point. The default is `https://docs.dataflair.ai`. |
| `--fixtures <folder>` | Replay a folder of fixtures instead of the bundled ones. |

The exit code is 0 when your endpoint is ready to connect, 1 when a case failed, and 2 when the command was used wrongly.

## A passing run

```text
✓ GET  /api/reports/affiliate-daily 200  Returns daily rows per affiliate
✓ GET  /api/reports/affiliate-daily 422  A date that cannot be read is a 400 or 422
✓ GET  /api/reports/affiliate-daily 403  A request with no credential is a 401 or 403
✓ GET  /api/reports/affiliate-daily 200  A range of one day returns rows for that day only
✓ GET  /api/reports/affiliate-daily 401  A wrong credential is a 401 or 403

5 passed · 0 failed. Ready to connect.
```

## A failing run

Here an endpoint ignores the `from` and `to` it was sent and answers with fixed rows:

```text
✗ GET  /api/reports/affiliate-daily 200  Returns daily rows per affiliate
    rows[0].date is 2026-01-01, outside the range you were asked for (2026-02-01 to 2026-02-07).
    rows[1].date is 2026-03-01, outside the range you were asked for (2026-02-01 to 2026-02-07).
    → See https://docs.dataflair.ai/stats/operator-api/contract#what-we-send
✓ GET  /api/reports/affiliate-daily 422  A date that cannot be read is a 400 or 422
✓ GET  /api/reports/affiliate-daily 403  A request with no credential is a 401 or 403
✗ GET  /api/reports/affiliate-daily 200  A range of one day returns rows for that day only
    rows[0].date is 2026-01-01, outside the range you were asked for (2026-02-01 to 2026-02-01).
    rows[1].date is 2026-03-01, outside the range you were asked for (2026-02-01 to 2026-02-01).
    → See https://docs.dataflair.ai/stats/operator-api/contract#what-we-send
✓ GET  /api/reports/affiliate-daily 401  A wrong credential is a 401 or 403

3 passed · 2 failed. Not ready to connect.
```

## What it sends

The request is the URL you gave, with the query string removed and `from` and `to` added. DataFlair does the same. The path is kept exactly as you wrote it. The `from` and `to` come from the fixtures: `2026-02-01` to `2026-02-07`, and one day for the single-day case.

The check sends five requests. The credential-free case sends no credential. The wrong-credential case sends a generated one. Nothing is written on your side, because the endpoint is read-only.

## What it checks

Every case checks that your endpoint answered and that the status is the expected one. The cases that expect `200` also check the body against the schema, and these rules:

| Rule | What it checks | Explained in |
| --- | --- | --- |
| `operator.firstRowHasMetric` | The first row has at least one of `registrations`, `ftds`, `clicks`, `deposit_amount`, `reported_ngr`, `commission_amount` or `kyc_verified_ftds`. Without one, DataFlair fails the whole pull as a schema mismatch. | [Fields](/stats/operator-api/contract#fields) |
| `operator.rowsInRange` | Every row is for a day between `from` and `to`, both inclusive. | [What we send](/stats/operator-api/contract#what-we-send) |

The schema requires `date` and `affiliate_id` on every row, a `date` in the form `YYYY-MM-DD`, integer counts, and numeric amounts. DataFlair skips a row that has no `date` or `affiliate_id`, so the check fails it instead of letting it pass.

If your URL redirects, the check stops at the redirect. DataFlair follows up to five redirects when it pulls, but the check does not follow any, so run it against the final URL.

## What it does not check

* It does not check that your numbers are right, or that the timezone of your days is UTC. See [Timezone and backfill](/stats/operator-api/timezone-and-backfill).
* It does not test a large range, a `429`, a `5xx` or a slow answer. DataFlair retries a `429` and a `5xx`. The check does not.
* It does not test that an affiliate sees only their own rows. That happens inside DataFlair.

If a case fails, follow its link. The [contract](/stats/operator-api/contract) covers the rest.
