---
url: https://docs.dataflair.ai/marketplace/ad-server-api/operations/reports.md
description: >-
  Impressions and clicks per booked line over a date range. DataFlair uses them
  to reconcile and settle a campaign.
---

# POST /reports: Delivery report

After a campaign goes live, DataFlair pulls delivery from your ad server. It compares what was booked with what served, and it settles the campaign. DataFlair needs **impressions and clicks for each booked line**, so every number ties back to the slot the advertiser paid for.

Google Ad Manager's REST reporting does the same job with a report grouped by `LINE_ITEM_ID`. Revive does it with `bannerDailyStatistics`, which gives impressions and clicks per banner.

## What we send

DataFlair names the lines it wants by the `line_item_id` values you returned from `POST /orders`, or by the `order_id`. Support at least the `line_item_ids` form.

::: code-group

```http [HTTP]
POST /reports HTTP/1.1
Host: ads.example.com
Authorization: Bearer sk_live_9f2c…
Content-Type: application/json
```

```bash [curl]
curl -X POST "$BASE_URL/reports" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json
```

:::

The body, saved as `request.json`:

::: code-group

```json \[exemplary/reports/request.json]
{
  "line_item_ids": [
    "li_88012",
    "li_88013"
  ],
  "date_range": {
    "start_date": "2026-08-01",
    "end_date": "2026-08-31"
  },
  "granularity": "total"
}

```

:::

The JSON on this page is read from the [fixture files](/marketplace/ad-server-api/fixtures), so it cannot drift from the test suite.

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `line_item_ids` | array of strings | one of these two | The lines to report on: the ids you returned from `POST /orders`. |
| `order_id` | string | one of these two | Report on every line in an order instead. Some publishers accept this in addition. Do not rely on it without agreeing it with the publisher first. |
| `date_range.start_date`, `date_range.end_date` | string | yes | Inclusive, `YYYY-MM-DD`, in your account timezone. |
| `granularity` | string | no | `total` (one row per line, the default) or `daily` (one row per line per day). |
| `limit` | integer | no | Page size when the result is large enough to page. Default 50, maximum 200. See [Pagination](/marketplace/ad-server-api/conventions#pagination). |
| `cursor` | string | no | The opaque cursor from a previous response's `next_cursor`. |

## What you return

Status `200`. With `granularity: total`:

::: code-group

```json \[exemplary/reports/expected-response.json]
{
  "rows": [
    {
      "line_item_id": "li_88012",
      "impressions": 41830,
      "clicks": 51
    },
    {
      "line_item_id": "li_88013",
      "impressions": 12004,
      "clicks": 9
    }
  ]
}

```

:::

With `granularity: daily`:

::: code-group

```json \[supplemental/reports-daily/expected-response.json]
{
  "rows": [
    {
      "line_item_id": "li_88012",
      "date": "2026-08-01",
      "impressions": 1421,
      "clicks": 2
    },
    {
      "line_item_id": "li_88012",
      "date": "2026-08-02",
      "impressions": 1550,
      "clicks": 1
    }
  ]
}

```

:::

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `rows` | array | yes | One row per line for `total`, one per line per day for `daily`. Every row in a response has the same shape. |
| `rows[].line_item_id` | string | yes | **The same stable id you returned when you created the line.** It is the reconciliation key. |
| `rows[].date` | string | only for `daily` | `YYYY-MM-DD`. |
| `rows[].impressions` | integer | yes | Ad-server impressions delivered for this line. |
| `rows[].clicks` | integer | yes | Ad-server clicks recorded for this line. |
| `next_cursor` | string or null | no | A top-level sibling of `rows`. Include it when the result is paged. Leave it out, or return `null`, on the last page. |

## The reconciliation key

DataFlair matches every row to a booked slot **by `line_item_id`**. It does not match by name, position or slot. This is why the id must be stable (see [Concepts](/marketplace/ad-server-api/concepts#the-two-ids-that-make-reconciliation-work)). A line renamed or edited in your console must keep reporting under the same `line_item_id`. Google Ad Manager works the same way. Renaming a line in GAM does not break DataFlair's reconciliation.

DataFlair sets aside a row whose `line_item_id` it does not recognize as "unmatched delivery". It does not force the row onto a booking. Return ids exactly as you issued them.

## Return ad-server-only numbers

Return the impressions and clicks **your ad server served for this line**. Do not return blended totals that include other demand.

GAM shows why. It exposes a blended `IMPRESSIONS` and `CLICKS` pair, which includes AdSense, Ad Exchange and yield-group fill. It also exposes an ad-server-only `AD_SERVER_IMPRESSIONS` and `AD_SERVER_CLICKS` pair. Only the ad-server-only pair matches a reserved line item. The blended pair absorbs unrelated network delivery. DataFlair reconciles a reserved booking, so it needs the ad-server-only figures.

* **Impressions and clicks only.** DataFlair works out CTR itself and does not trust a reported CTR. No revenue or cost field is needed. Billing happens entirely in DataFlair.
* **Counts, as integers.** Do not return currency values.

## Delivery is publisher-reported

The numbers you return become the billing reference for the campaign. DataFlair cross-checks them against its own tracked counts: clicks through the `/go/{code}` link that every campaign serves through, and impressions through the pixel inside the ad frame. DataFlair reviews a large gap before it settles. Report the same figures your own console shows your ad-ops team.

Reporting is a pure read, so DataFlair can call it as often as it needs without side effects.

## Push alternative

The primary model is pull. DataFlair calls `POST /reports` on a schedule. Its GAM reimport runs every 15 minutes in production, and a similar cadence is fine here. If you prefer to push daily delivery to DataFlair, raise it with DataFlair. A webhook-style ingest is a possible addition. The pull model is the supported default, and it needs nothing from you beyond this endpoint.

## Errors

| Status | What caused it | What DataFlair does | What to do |
| --- | --- | --- | --- |
| `400` | Malformed request. | Treats it as a bug, logs it and shows it. | Check the body against the fields above. |
| `401` | Missing, invalid or expired key. | Marks the connection as `error`. | Check the key. |
| `403` | The key lacks a required scope. | Marks the connection as `error`. | Widen the key's scope. |
| `404` | Unknown `line_item_id` or `order_id`. | Skips the item and reports it. | Return `404` only for an id that does not exist. |
| `422` | Valid shape, invalid values, such as a bad date range. | Shows your `message`, so the operator can fix it. | Return a clear `message`. |
| `429` | Rate limit exceeded. | Backs off and retries per `Retry-After`. | Send `Retry-After` in seconds. |
| `5xx` | Server error. | Retries with backoff. | Check your server's logs. |

Every error body has the shape described in [Conventions](/marketplace/ad-server-api/conventions#error-model).

## Timeouts and retries

DataFlair does not call this endpoint yet. The platform's configured timeouts for calls to your server are 10 seconds to connect and 30 seconds in total.

## Verify

Run the [conformance check](/marketplace/ad-server-api/conformance) with `--only reports` to check this endpoint against the [fixtures](/marketplace/ad-server-api/fixtures).
