These are different data products. A CSV aggregate is not a raw event feed, and
a notification is not a reporting export.
1. Prove access and choose the report
For MCP, callget_status and verify the active account. Inspect the current
get_delivery schema before choosing a report.
- A Buyer Account uses
report: "live_campaign_delivery"for one exact connected-provider campaign, orreport: "campaign_delivery"for stored aggregate rows. - A Seller Account uses
report: "delivery"for seller-reported delivery, orreport: "margin"for its cumulative spread ledger. - An integrated Media Company can also read
live_campaign_deliveryfor one exact own-supply campaign. This does not grant a Seller credential general access to Buyer REST reporting.
2. Read a bounded delivery window
For a stored Buyer Account aggregate, callget_delivery:
range: {"lifetime": true}; a packageId filter requires
a bounded range instead. Follow the returned pagination contract until all
requested rows are read.
Delivery summaries label explicit periods as UTC dates. They do not convert the
requested reporting range to a campaign or advertiser timezone.
Metrics use AdCP’s delivery metric names:
A metric the seller did not report is returned as unavailable, never as zero.
Viewability, for example, is unavailable for delivery whose seller measured none.
Every row and total carries a
finality label: Official (final, eligible for
billing), Snapshot (provisional), or unavailable when the delivery has no
revision evidence. Figures of different finality are never added together.
Rows are split by finality, and when matched delivery spans more than one
class, totals gives no figure (each metric reads finality_mixed) and
totalsByFinality gives one total per class.
For the current connected-provider response instead, use the additive live
mode:
LIVE_PROVIDER_UNAVAILABLE instead of NOT_FOUND. The response
lists the campaign’s buys under details.mediaBuys with each buy’s status
(such as canceled) and, when one is recorded, an errorCode (such as
source_unavailable). The list holds at most 25 buys, newest first;
details.mediaBuyCount is the total, so a count above 25 means the list is
cut short. Tell
the person there is nothing live to report. Use campaign_delivery for
delivery stored before the buys ended. NOT_FOUND still means the campaign
does not exist in the active account. If Apostra cannot check the campaign’s
state at that moment, the call returns a retryable SERVICE_UNAVAILABLE
instead, and neither answer is known.
The same LIVE_PROVIDER_UNAVAILABLE answer covers a campaign that has not
started delivering yet. When the provider cannot return delivery, whatever the
way it fails (a refusal, a failed or incomplete report, an error), and every buy
is either waiting to start (a draft, pending approval, or a flight start still
ahead) or has ended, the message says what each waiting buy is waiting on, for
example that no creatives are attached yet, and the start time of a flight that
has not begun. Tell the person nothing has delivered yet, and read live delivery
again once the flight has started. A campaign with any buy whose flight has
started does not get this answer, and neither does a refused credential or a
rate limit, which keep their own errors. If the provider can answer, including
with zeros, its answer is returned.
If the provider refuses the read for a campaign that has started, the call
returns SERVICE_UNAVAILABLE with details.reason of
provider_rejected_delivery_read and the provider’s own code in details.code.
This is the provider’s answer, not a statement that your input is wrong: the
read carries only the campaign and the date range you gave. Retry later, and if
it keeps failing, check the campaign at the provider or contact support. A
refused credential is a different failure and keeps its own error.
A campaign whose media buy has no stored provider route or identity cannot be
read at all, and retrying cannot change that. The call returns
SERVICE_UNAVAILABLE with details.reason of
provider_media_buy_identity_missing, recovery: "terminal" and a suggestion
not to retry. Use campaign_delivery for delivery stored earlier, and ask
Apostra Support to repair the media buy’s provider binding or recreate the
campaign. Use the
buyer or
seller reference for
the report available to your account.
Preserve the report type, account and resource scope, requested dimensions or
provider reporting period, currency, and time of extraction with the result.
3. Preserve what the data means
Your consumer should carry coverage, warnings, finality, and unavailable values alongside the numbers. Do not turn a missing source value into zero or describe seller-reported delivery as independent buyer measurement. Finality alone is not evidence that an amount is eligible for billing. A storedcampaign_delivery read returns rows only for campaigns whose delivery
has reached Apostra. An account-wide read can therefore leave out a campaign
that was in flight during the range. That campaign is missing, not at zero.
On an account-wide read over a bounded range, the response carries coverage
so you do not have to work out which campaigns are missing:
inFlightCampaignscounts the campaigns in scope with a current media buy that was booked or running during the range. Draft, failed, rejected and canceled buys do not count.withoutStoredRowslists the in-flight campaigns that have no stored rows, up to 20.countis the full number, andtruncatedis true when the list is shorter thancount.- Each
nextActionis the exact call that reads that campaign from its connected provider. The stored rows, totals and paging are unchanged.
coverage appears only when the read is account-wide: a bounded date range with
no filter other than advertiserId. An advertiserId filter narrows the list
to that advertiser. A read filtered to one campaign, media buy, package,
product or channel group, and a lifetime read, return no coverage.
For stored Buyer campaign delivery, totals covers every matched row, not just
the current page. Do not add the repeated totals from successive pages. Unless
you send currency, mixed currency results are not converted: monetary totals
can be unavailable while counts remain usable. Keep each row’s currency with its
monetary values. To read every row and the totals in one currency, see
Converting into one currency.
Conversions already applied
Stored Buyer campaign delivery reports each media buy’s spend in its advertiser’s currency. When a seller reports a buy’s spend in another currency, Apostra converts it before the rows are built, sospend, conversion_value
and the cost metrics on those rows are converted figures, not the numbers the
seller sent. The response states every conversion it applied in
currencyConversions:
- There is one entry for each media buy behind the page’s rows whose spend was
converted, ordered by
mediaBuyId. The field is absent when none was. fromCurrencyis the currency the spend was in before conversion: the currency the seller reported, or the currency the seller is paid in when the seller’s label disagrees with it.toCurrencyis the advertiser’s currency.rateis the number oftoCurrencyunits per onefromCurrencyunit.rateSourcesays where the rate came from:booked: the rate locked when the buy was booked.snapshot: Apostra’s daily rate store, for the day the buy was created, used for a buy booked without a locked rate.settlement: the seller labelled the spend with a currency it is not paid in. Apostra takes the spend in the currency the seller is paid in, which is the advertiser’s currency, at a rate of 1.fromCurrencyandtoCurrencyare then the same, andrateDateis null because no rate was looked up.
rateDateis the UTC date of the rate.- The list holds at most 100 entries. When more buys behind the page were
converted,
currencyConversionsOmittedgives the number left out; it is absent otherwise. Group bymedia_buyor narrow the filters to see each one. - The entries cover the buys behind this page’s rows, while
totalscovers every matched row, so a converted buy that appears only on a later page is in the totals but listed on that later page.
Converting into one currency
Sendcurrency with report: "campaign_delivery" to read money on every row
and on the totals in one currency:
currency is an uppercase ISO-4217 code, one of AUD, BRL, CAD, CHF, CNY, DKK,
EUR, GBP, HKD, IDR, INR, JPY, KRW, MXN, NOK, NZD, PLN, SEK, SGD, USD and ZAR:
the currencies Apostra’s daily rate store prices. Any other code is refused. It
applies to campaign_delivery only. live_campaign_delivery, seller
delivery and margin refuse it: the live report is the seller’s own payload,
and seller reports are not converted.
The conversion happens after conversions already applied,
so it starts from each row’s advertiser currency:
- Each day converts at that day’s rate. A row that spans several days,
because
dateis not one of its dimensions, is converted day by day and then added up. A range that spans a currency move therefore reads correctly. - Rates come from the daily rate store. It holds one rate per currency per UTC day against USD, and other pairs are crossed through USD. When a day has no rate, the most recent rate from up to 7 days before is used. Reading delivery never creates a rate.
- Counts do not change. Impressions, clicks, conversions and the other
counts are never converted.
cpm,cost_per_click,cost_per_acquisitionandroasare worked out again from the converted money. - A row with a day that has no rate is not converted. It keeps its own
currencyand values, so a row whosecurrencydiffers from the one you asked for was not converted.totalsthen reports money metrics as unavailable withreason: "fx_rate_unavailable"andcurrencyas null, rather than adding a partial sum. Counts still total. - Rows that differed only by currency become one row once both are converted.
- Goal progress is not converted.
goalProgressstays in each campaign’s own currency.
conversion:
rateBasisis alwaysdailyandrateSourceis alwayssnapshot, the daily rate store.carriedForwardDaysis the most days any rate used was carried forward from an earlier day, at most 7. It is 0 when every rate was the day’s own.unconvertedlists each source currency and day that had no rate, ordered by currency then date, at most 100.unconvertedOmittedgives the number left out and is absent otherwise. It covers every matched row, liketotals.
conversion is absent when you do not send currency, and the response is
then exactly what it is without the option. A nextCursor belongs to the query
that returned it, including its currency. If the rates cannot be read, the
call returns SERVICE_UNAVAILABLE; retry, or omit currency to read each row
in its own currency.
Converted figures are for reporting. Billing keeps the rate locked when a buy
was booked, so a converted spend can differ from what is invoiced.
Live Buyer campaign delivery does not come from Apostra’s reporting store and
does not claim reporting-pipeline finality. Keep the returned currency and any
unavailable values or warnings with the provider totals.
Reach and frequency
Reach counts unique people, households or devices, so it cannot be added up the way impressions can. Apostra reports the reach a seller sent and never works one out.- Where it is available.
live_campaign_deliveryreturnsreach,frequency,reach_unitandreach_windowas the seller reported them, on the media buytotalsand on each package inby_package. The same values are in thedeliverypayload and indeliverySummary. Storeddeliveryandcampaign_deliveryreads do not return reach yet, so a stored read has no reach number to compare. - Absent is not zero. A seller that sent no reach leaves the fields out. Do
not read a missing
reachorfrequencyas0. - Read the unit and the window with the number.
reach_unitsays what is counted:individuals,households,devices,accounts,cookiesorcustom. Compare reach only between values with the same unit.reach_windowsays what period the number covers:cumulativeis everyone since the campaign started,periodis one non-overlapping period such as a single day, androllingis a trailing period ending at the report.periodandrollingcarry the length inreach_window.period. A value with noreach_windowhas an unspecified window. - Never add reach up. Do not sum
reachacross days, packages or media buys. The same person appears in many of them, so a sum counts people more than once. Acumulativevalue replaces the earlier one, and arollingvalue stands on its own. Do not averagefrequencyacross rows either. - A buy-level value and a package value are separate reports. A seller may report reach for the whole media buy and for each package. The package values do not add up to the media buy’s reach.
4. Run a repeatable extraction job
For a daily dashboard or warehouse load, your worker can:- Load the account, report, requested dimensions, and last completed date window from its checkpoint.
- Read each bounded window and all its pages into a staging destination.
- Check warnings and coverage, then publish the completed window. Use a key containing its account, dimensions, date, and currency so a retry replaces the same data rather than appending duplicates.
- Advance the checkpoint only after the destination has accepted the complete window.
- Re-read a recent overlap window according to your source’s correction and freshness needs. Retain when each extract was observed.
5. Choose CSV export or scheduled cloud delivery
For a CSV aggregate, use the current v2 REST reporting contract:downloadUrl, expiresAt, fileName, and
rowCount. Fetch the file in the consuming service; the signed URL is a bearer
credential and must not appear in logs or public dashboards. See
CSV export.
For a recurring cloud feed, follow Data Delivery:
grant the documented storage permissions, register an advertiser-scoped
credential, wait for its asynchronous validation, then attach outputs for the
supported data types and cadence. Confirm that objects arrive in your bucket
or container before treating the feed as working. Data Delivery uses v2
configuration today; it is not a generic v3 export tool.
Data Delivery credentials and outputs are full-replace arrays. Read the current
configuration and preserve entries you intend to keep before updating it.