Skip to content
POST/reports We call you

DataFlair calls YOUR server. You implement this endpoint. You do not call DataFlair.

RequiredBuild this. It is part of the contract.

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.

http
POST /reports HTTP/1.1
Host: ads.example.com
Authorization: Bearer sk_live_9f2c…
Content-Type: application/json
bash
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:

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, so it cannot drift from the test suite.

FieldTypeRequiredMeaning
line_item_idsarray of stringsone of these twoThe lines to report on: the ids you returned from POST /orders.
order_idstringone of these twoReport 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_datestringyesInclusive, YYYY-MM-DD, in your account timezone.
granularitystringnototal (one row per line, the default) or daily (one row per line per day).
limitintegernoPage size when the result is large enough to page. Default 50, maximum 200. See Pagination.
cursorstringnoThe opaque cursor from a previous response's next_cursor.

What you return ​

Status 200. With granularity: total:

json
{
  "rows": [
    {
      "line_item_id": "li_88012",
      "impressions": 41830,
      "clicks": 51
    },
    {
      "line_item_id": "li_88013",
      "impressions": 12004,
      "clicks": 9
    }
  ]
}

With granularity: daily:

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
    }
  ]
}
FieldTypeRequiredMeaning
rowsarrayyesOne row per line for total, one per line per day for daily. Every row in a response has the same shape.
rows[].line_item_idstringyesThe same stable id you returned when you created the line. It is the reconciliation key.
rows[].datestringonly for dailyYYYY-MM-DD.
rows[].impressionsintegeryesAd-server impressions delivered for this line.
rows[].clicksintegeryesAd-server clicks recorded for this line.
next_cursorstring or nullnoA 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). 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 ​

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

Every error body has the shape described in Conventions.

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 with --only reports to check this endpoint against the fixtures.

Verify this endpoint

node tools/adserver-check/bin/adserver-check.mjs http://localhost:3000/api/v1 --key sk_test_local --only reports

No playground here: the request would go to your own server. Run the checker.

The checker is not published to npm yet. See how to run it.

Docs version 1.0.1