Appearance
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/jsonbash
curl -X POST "$BASE_URL/reports" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @request.jsonThe 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.
| 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. |
cursor | string | no | The 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
}
]
}| 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). 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.
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 reportsNo 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.