> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Deliver reporting to Apostra

> How your sales agent gets delivery reports to Apostra, what a report must contain, how Apostra answers, and how to read the receipts for a media buy.

Buyers see what ran from the delivery your sales agent reports. Apostra gets it
one of two ways: your agent pushes each report to Apostra, or Apostra reads it
from your agent's `get_media_buy_delivery`. This page covers both, the report
your agent has to send, how Apostra answers it, and how to check what Apostra
received for a media buy.

## How Apostra gets your delivery

Your products decide it. Each product can declare
`reporting_capabilities.supports_webhooks` in `get_products`.

| Your products declare | What Apostra does |
| - | - |
| `supports_webhooks: false` on every product in a buy | Reads that buy's delivery from your agent by polling. It sends no `reporting_webhook` on the buy. |
| `supports_webhooks: true`, or nothing | Sends a `reporting_webhook` on `create_media_buy`. Your agent pushes reports to it. |
| A buy that mixes the two kinds of product | Keeps the `reporting_webhook` and treats the buy as pushed. |

Apostra does not read reporting files from your own storage, so there is no
bucket or file option. To send reports, push them. To have Apostra read them,
declare `supports_webhooks: false` and answer `get_media_buy_delivery`.

Certification judges the choice you make. See
[Reports arrive on time](https://docs.apostra.com/v3/certification.md#what-each-check-judges) and the
[delivery report format finding](https://docs.apostra.com/v3/certification.md#delivery-report-format-finding).

## Push reports to Apostra

When Apostra books on your agent, `create_media_buy` carries a
`reporting_webhook` with the `url` to post reports to. Post to that URL exactly
as it was sent, and sign every request. An unsigned or wrongly signed report
gets `401` and is not read. See
[Signing the webhooks you send us](https://docs.apostra.com/v3/seller-webhook-signing.md).

### Send the AdCP envelope

From AdCP 3.1, a delivery report is an MCP webhook envelope with
`task_type: "media_buy_delivery"` and the report under `result`:

```json theme={null}
{
  "idempotency_key": "rpt-2026-11-08-mb_123",
  "operation_id": "op_abc123",
  "task_id": "task_123",
  "task_type": "media_buy_delivery",
  "status": "completed",
  "timestamp": "2026-11-09T02:05:00Z",
  "result": {
    "notification_type": "scheduled",
    "reporting_period": {
      "start": "2026-11-08T00:00:00Z",
      "end": "2026-11-09T00:00:00Z"
    },
    "media_buy_deliveries": [
      {
        "media_buy_id": "mb_123",
        "status": "active",
        "currency": "USD",
        "totals": { "impressions": 120000, "spend": 840, "clicks": 960 },
        "by_package": [
          { "package_id": "pkg_1", "impressions": 120000, "spend": 840, "clicks": 960 }
        ],
        "daily_breakdown": [
          { "date": "2026-11-08", "impressions": 120000, "spend": 840, "clicks": 960 }
        ]
      }
    ]
  }
}
```

AdCP defines the shape, in `media-buy/media-buy-delivery-webhook-result.json`
for your release. Apostra checks it against every AdCP release from 3.1 that it
bundles, because a delivery webhook does not say which release it follows, and
refuses it only when none of them accepts it. These fields are required:

| Where | Required |
| - | - |
| The envelope | `idempotency_key`, `operation_id`, `task_id`, `task_type` (`media_buy_delivery`), `status` and `timestamp` |
| `result` | `notification_type`, `reporting_period` with `start` and `end`, and `media_buy_deliveries`. AdCP 3.1 also requires `currency` here. From 3.2 it sits on each delivery. |
| Each entry in `media_buy_deliveries` | `media_buy_id`, `status`, `totals` and `by_package` |

Send `by_package` on every entry. When you cannot split delivery by package,
send an empty array. Leaving it out is a `400`. Extra fields are kept, but they
never stand in for a required one: `by_day` does not replace `by_package`.

An AdCP 3.0 report, with `media_buy_deliveries` at the top level and no
envelope, is still accepted so your reporting keeps working. It shows as a miss
in the delivery report format finding. See
[Delivery reports](https://docs.apostra.com/v3/agent-webhook-deliveries.md#delivery-reports).

### Say which days the delivery is for

Send `daily_breakdown` on every entry, with one item per day and its `date` as
`YYYY-MM-DD`, so Apostra can record each day. A report whose
`reporting_period` is a single day names that day itself. Totals over several
days with no `daily_breakdown` get a `200`, but Apostra does not record them,
because it cannot tell which day they belong to.

Report a day after it has ended. A report for a day still in progress is
acknowledged, but Apostra keeps it only as provisional figures for the buy. It
is not recorded as that day's delivery, and package figures wait for the closed
day's report. A period that ends at the next midnight and lasts 23 to 25 hours
is open until that end. Any other period is open until 25 hours after its start.

Report only what you observed. A day, package or source you have no figures for
is not a zero, so leave it out rather than sending `0`. Report spend in the
currency the buy was booked in. A report in another currency is still recorded,
but Apostra cannot book its spend.

### How Apostra answers

| Answer | When | What to do |
| - | - | - |
| `200` | The report matched the schema, or is in the AdCP 3.0 format. Rows Apostra could not record still get `200`: a buy it has no record of under your source, or totals over several days with no `daily_breakdown`. | Nothing to retry. Send `daily_breakdown` on every entry so each day is recorded. |
| `400` | The envelope or its report does not match the schema. The message names the first failing field. | Fix the report and send the corrected one. The same body gets the same answer, so do not retry a `400`. |
| `401` | The signature, key, signer or nonce did not pass. The body never says which. | Fix the signing. See [Signing the webhooks you send us](https://docs.apostra.com/v3/seller-webhook-signing.md#responses). |
| `404` | A report signed with HMAC names a source that is not found or is no longer active. | Check that the source is still connected. Contact support if it is. |
| `413` | The report has more than 250 media buys. | Split it into several reports. |
| `503` | Apostra could not record the report yet. The response carries `Retry-After: 30`. | Send it again after that many seconds. Resending is safe. |

Every report Apostra refuses is in your agent's
[webhook results](https://docs.apostra.com/v3/agent-webhook-deliveries.md), with the first failing field.

### Correct a report

To correct a figure, send the cumulative-to-date report again with the right
value. Apostra keeps the latest report for each reporting day, media buy and
source, so a re-sent report replaces your own earlier one for that day. It does
not replace another source's report for the same buy and day. A retry after a
`503` therefore never counts twice.

## When Apostra reads your delivery

If your products declare `supports_webhooks: false`, Apostra calls your
`get_media_buy_delivery` for each live buy twice a day, at 02:00 and 14:00 UTC,
once the buy has delivery to read. Right after a buy is created it reads more
often while it waits for the first usable delivery.

* **Reads that return nothing usable.** When a read comes back empty, or for an
  active buy has no `daily_breakdown` yet, Apostra reads again soon after, on a
  backoff that grows from seconds to an hour. It makes six reads in all for that
  slot, then waits for the next one.
* **After a buy completes.** Apostra keeps reading it twice a day for seven
  days, to pick up late or revised reporting, and then stops.
* **The schedule is fixed.** The frequencies a product declares in
  `reporting_capabilities` do not change when Apostra reads you. They set what
  certification expects.
* **Delivery by property is asked for only when you declare it.** When every
  product on a buy declares `supports_property_breakdown` in
  `reporting_capabilities`, the read also carries `reporting_dimensions.property`
  with a `limit` of 250 rows per package. Answer it with `by_property` on every
  package, with `by_property_truncated` and `by_property_suppressed`. A package
  that comes back without them counts in the
  [property delivery finding](https://docs.apostra.com/v3/agent-requirements.md#property-delivery-finding).
* **The reply must be valid AdCP.** Apostra checks it against the AdCP
  `get_media_buy_delivery` response. Each `by_package` item needs `package_id`,
  `spend`, `pricing_model`, `rate` and `currency`. A reply that fails counts in
  [valid responses](https://docs.apostra.com/v3/agent-requirements.md#valid-responses-finding).

## Google Ad Manager storefronts Apostra runs

When your Storefront sells Google Ad Manager (GAM) inventory through the sales
agent Apostra runs for you, that agent reports your delivery from GAM in two
ways: provisional figures about every 15 minutes while a buy runs, shown in
[Campaign Pulse](https://docs.apostra.com/v3/campaign-pulse.md), and daily figures read twice a day. See
[How often Apostra reads GAM delivery](https://docs.apostra.com/v3/seller-reporting-delivery.md#how-often-apostra-reads-gam-delivery).
Each daily read runs a new GAM report, so nothing is cached between those reads.

### Impressions and clicks are filtered for invalid traffic

Delivery `impressions` and `clicks` are GAM's ad-server counts,
`AD_SERVER_IMPRESSIONS` and `AD_SERVER_CLICKS`, which already leave out the
traffic GAM judged invalid. Google says Ad Manager "discards impressions and clicks
that are considered invalid"
([how Ad Manager counts impressions and clicks](https://support.google.com/admanager/answer/2521337)),
and that its metric totals are net of General Invalid Traffic (GIVT) filtration
and some Sophisticated Invalid Traffic (SIVT) filtering techniques
([Ad Manager's invalid traffic methodology](https://support.google.com/admanager/answer/7214243)).
These filtered counts are the impressions and clicks buyers see.

Your products also report two AdCP vendor metrics from `google.com`:
`unfiltered_impressions` and `unfiltered_clicks`. They are GAM's counts before
invalid-traffic filtering (`AD_SERVER_UNFILTERED_IMPRESSIONS` and
`AD_SERVER_UNFILTERED_CLICKS`), reported beside the filtered counts for
comparison. Each product lists them in `reporting_capabilities.vendor_metrics`,
and each package's delivery carries them in `vendor_metric_values`. The gap
between the unfiltered and filtered counts is the traffic GAM judged invalid.
The [ad-server delivery check](https://docs.apostra.com/v3/certification.md#what-each-check-judges) in
certification compares our tracker with `unfiltered_impressions`.

### How often Apostra reads GAM delivery

Buyers get your GAM delivery in two forms: provisional figures within minutes,
and daily figures once GAM has counted each day.

**Provisional figures, about every 15 minutes.** While a buy runs, the sales
agent re-reads the delivery counts of your live GAM line items, those
delivering or ready to deliver, about every 15 minutes. It reads the counts GAM
keeps on each line item rather than running a report, so it uses none of your
report quota. Apostra reads them from the agent every 5 minutes, as each
package's snapshot (`get_media_buys` with `include_snapshot`), so a new count
reaches buyers within about 5 minutes. A buy is read this way from the start of
its flight for as long as the agent reports it active. You don't need to set
anything up.

The snapshot holds each package's running totals since the flight started:
impressions, clicks and spend. Buyers see it in
[Campaign Pulse](https://docs.apostra.com/v3/campaign-pulse.md), labelled provisional. It is not final and
not billable, and it is kept apart from the daily figures: it never replaces or
adds to them. The two come from different GAM reads, and GAM can still revise
its counts, so the daily figures for a day can differ from the provisional ones
buyers saw during it.

**Daily figures, twice a day.** GAM needs about four hours to count
delivery. So your GAM products declare `expected_delay_minutes` of 240, or more
if you set a longer delay, report daily in your network's timezone, and declare
`supports_webhooks: false`. Apostra therefore reads your delivery on the
schedule in
[When Apostra reads your delivery](https://docs.apostra.com/v3/seller-reporting-delivery.md#when-apostra-reads-your-delivery):

* **Twice a day.** Apostra reads each live buy at 02:00 and 14:00 UTC.
* **Until the first figures land.** Until a buy has its first usable delivery,
  Apostra reads again on a backoff that grows to an hour, up to six reads for
  each slot.
* **For seven days after a buy completes.** Apostra keeps reading twice a day,
  to pick up GAM's revisions, and then stops.

Each read brings in what GAM had counted by then, about four hours behind, so
buyers see a day's figures fill in over the reads during and after it. How long
before buyers see the whole day depends on your network's timezone, because GAM
counts a day in it: the day is whole at the first read at least four hours
after it ends. For a network in the Americas or Europe, that is usually the
14:00 UTC read the next day. For one in Asia or Australia, it is the 02:00 UTC
read the next day.

To ask Apostra to read a buy again, use
[Ask Apostra to read again](https://docs.apostra.com/v3/seller-reporting-delivery.md#ask-apostra-to-read-again).

GAM delivery is not synced by a separate reporting sync: Apostra reads it live,
so a reporting sync with no runs on a GAM source is expected, not a fault.

## Reporting receipts

A reporting receipt is read-only evidence of one report Apostra received for a
buyer's media buy, whether your agent pushed it or Apostra read it. It helps you
confirm that a report arrived and see how Apostra handled it. It is not the
buyer's delivery result, a billing record, or a statement that reporting is
final.

### Read receipts

Call `get_seller_reporting_receipts` as a Seller Account:

```json theme={null}
{
  "accountId": "SELLER_ACCOUNT_ID",
  "mediaBuyId": "MEDIA_BUY_ID",
  "buyerAccountId": 123
}
```

`buyerAccountId` is the Buyer Account that owns the media buy. The tool opens
the shared **Media Buy Timeline** Glance and returns the same data.

You can read a media buy only if that buyer account booked it with you.
Otherwise the tool answers `NOT_FOUND`, which does not say whether the buy
exists. The buyer account also needs reporting status. A buyer account on the
standard buyer profile, which new buyer accounts get by default, has it, and so
do buyers in the V3 beta. A buyer account on an earlier profile does not, and for
its media buys the tool answers `FEATURE_NOT_ENABLED`. You cannot change a
buyer's profile.

The result holds:

* `receipts`: one item per report, newest first. Each has a `receiptId` you can
  match to the report your agent sent, `receivedAt`, and a `transport`: a report
  your agent pushed is `storefront_source_push`, and a read Apostra made is
  `storefront_route_poll`. The values `webhook`, `poll` and `bucket` come from
  older reporting paths.
* `acceptance` on each receipt: a `state` and, when there is one, a `reason`
  with a `code`, the first error your source reported as `sourceCode`, and its
  `message`.
* `receivedTotals` and `receivedDailyTotals` on each receipt: the values that
  arrived in that report, exactly as your report expressed them. They cover
  `spend`, `impressions`, `clicks`, `videoCompletions`, `views`, `conversions`,
  `leads` and `currency`, as strings so you can compare them without losing
  precision. A later correction does not rewrite them.
* `computedDailyTotals`: Apostra's current value for each day and package, with
  the time it was `computedAt`. A later report can correct a computed day, so
  these can differ from an earlier receipt. Use the receipt to trace the input
  and the computed totals to see Apostra's present view.
* `canonical`: the current reporting status for the media buy. See
  [Reliable Reporting registration](https://docs.apostra.com/v3/seller-reliable-reporting-registration.md).
  Reading it does not request a new report.

### Receipt states

| State | `reason` | Meaning |
| - | - | - |
| `accepted` | none, or `processing_partial_success` | Apostra took the report in. With `processing_partial_success`, part of it was not processed: read `sourceCode` and `message`. |
| `rejected` | `ingestion_failed` or `processing_failed` | Apostra did not accept the report. The reason names the problem when Apostra has one. |
| `pending` | `polling_scheduled`, `polling_delayed` or `unrecognised_processing_status` | Apostra has not finished deciding. A read may be scheduled or may have come back with nothing yet. |

A state says what happened to that receipt. It does not say that every required
report arrived, that coverage is complete, or that the media buy is final, and
it is not proof that spend is billable.

### Limits

A result is bounded so it is never mistaken for a reporting export. It returns
the newest five receipts, up to fourteen received daily totals for each receipt,
and the newest thirty-one computed daily totals. When more exists, the matching
flag is `true`: `receiptsTruncated`, `receivedDailyTotalsTruncated` on the
receipt, or `computedDailyTotalsTruncated`.

In the Timeline, the **Reporting receipts** section shows three receipts at a
time and lets you page through the ones returned. It also has controls for the
two requests below.

## Ask Apostra to read again

After you read receipts, `request_seller_reporting_update` asks Apostra to read
a media buy's reporting again. It never calls your agent itself: Apostra's
scheduler makes the read. It takes the same `accountId`, `mediaBuyId` and
`buyerAccountId`, and the same access rules, as the receipts read.

```json theme={null}
{
  "accountId": "SELLER_ACCOUNT_ID",
  "mediaBuyId": "MEDIA_BUY_ID",
  "buyerAccountId": 123,
  "action": { "kind": "refresh" }
}
```

* **`refresh`** asks for the next read of reporting periods that have ended and
  are not already queued. At most 25 of your refresh requests can be waiting at
  one time. The result gives `queuedReportingItemCount` and that limit as
  `requestLimit`.
* **`replay`** asks for a read of a range of days. Add
  `"action": { "kind": "replay", "startDay": "2026-11-01", "endDay": "2026-11-07" }`.
  The range is 1 to 31 calendar days, and a request covers at most 25 reporting
  items. Apostra accepts at most two replay requests from you in any 24 hours.
  A third comes back with `rateLimited: true` and nothing queued.

A replay never changes a day that is already final. Those days come back in
`refusedFinalDays`. Items already waiting for a read are counted in
`alreadyQueuedReportingItemCount` and are not queued again. Both requests act on
the reporting items Apostra has recorded for the buy. If it has recorded none,
`queuedReportingItemCount` is 0. Neither changes buyer permissions or billing.

## Related

* [Signing the webhooks you send us](https://docs.apostra.com/v3/seller-webhook-signing.md) covers how to sign a push.
* [Webhook results](https://docs.apostra.com/v3/agent-webhook-deliveries.md) lists every report Apostra refused and why.
* [Reliable Reporting registration](https://docs.apostra.com/v3/seller-reliable-reporting-registration.md) covers reporting status and revisions.
* [Certification](https://docs.apostra.com/v3/certification.md) and [agent requirements](https://docs.apostra.com/v3/agent-requirements.md) say how reporting is judged.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.