Skip to main content
You can use Apostra as the source for a dashboard, a daily reporting job, or an agent that checks campaign performance. Start by deciding whether you need an interactive aggregate, a downloadable aggregate, or a recurring data feed. 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, call get_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, or report: "campaign_delivery" for stored aggregate rows.
  • A Seller Account uses report: "delivery" for seller-reported delivery, or report: "margin" for its cumulative spread ledger.
  • An integrated Media Company can also read live_campaign_delivery for one exact own-supply campaign. This does not grant a Seller credential general access to Buyer REST reporting.
For REST, use a credential authorized for the relevant Buyer API. A v3 MCP OAuth token cannot be used on REST. See Authentication.

2. Read a bounded delivery window

For a stored Buyer Account aggregate, call get_delivery:
Replace the campaign ID and dates with the scope you intend to read. Explicit date ranges are inclusive UTC dates and support at most 90 days. Buyer campaign delivery also supports 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:
The live mode accepts one exact campaign and no metrics, dimensions, other filters, or cursor. It preserves the validated provider response. When the campaign exists but none of its media buys is live at a provider, for example because every buy was cancelled or its seller became unavailable, the call returns 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 stored campaign_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:
  • inFlightCampaigns counts 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.
  • withoutStoredRows lists the in-flight campaigns that have no stored rows, up to 20. count is the full number, and truncated is true when the list is shorter than count.
  • Each nextAction is 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, so spend, 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.
  • fromCurrency is 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. toCurrency is the advertiser’s currency.
  • rate is the number of toCurrency units per one fromCurrency unit.
  • rateSource says 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. fromCurrency and toCurrency are then the same, and rateDate is null because no rate was looked up.
  • rateDate is the UTC date of the rate.
  • The list holds at most 100 entries. When more buys behind the page were converted, currencyConversionsOmitted gives the number left out; it is absent otherwise. Group by media_buy or narrow the filters to see each one.
  • The entries cover the buys behind this page’s rows, while totals covers every matched row, so a converted buy that appears only on a later page is in the totals but listed on that later page.
The conversions are stated whether or not you need them. Keep each entry with the rows of its media buy so a converted figure is never read as the seller’s own number.

Converting into one currency

Send currency 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 date is 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_acquisition and roas are worked out again from the converted money.
  • A row with a day that has no rate is not converted. It keeps its own currency and values, so a row whose currency differs from the one you asked for was not converted. totals then reports money metrics as unavailable with reason: "fx_rate_unavailable" and currency as 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. goalProgress stays in each campaign’s own currency.
The response says what it did in conversion:
  • rateBasis is always daily and rateSource is always snapshot, the daily rate store.
  • carriedForwardDays is 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.
  • unconverted lists each source currency and day that had no rate, ordered by currency then date, at most 100. unconvertedOmitted gives the number left out and is absent otherwise. It covers every matched row, like totals.
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_delivery returns reach, frequency, reach_unit and reach_window as the seller reported them, on the media buy totals and on each package in by_package. The same values are in the delivery payload and in deliverySummary. Stored delivery and campaign_delivery reads 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 reach or frequency as 0.
  • Read the unit and the window with the number. reach_unit says what is counted: individuals, households, devices, accounts, cookies or custom. Compare reach only between values with the same unit. reach_window says what period the number covers: cumulative is everyone since the campaign started, period is one non-overlapping period such as a single day, and rolling is a trailing period ending at the report. period and rolling carry the length in reach_window.period. A value with no reach_window has an unspecified window.
  • Never add reach up. Do not sum reach across days, packages or media buys. The same person appears in many of them, so a sum counts people more than once. A cumulative value replaces the earlier one, and a rolling value stands on its own. Do not average frequency across 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.
Seller delivery pages are live reads. A cursor continues the query but does not freeze a historical snapshot. If your application needs a reproducible extract, store the completed extract and its query metadata in your own system and label when it was read.

4. Run a repeatable extraction job

For a daily dashboard or warehouse load, your worker can:
  1. Load the account, report, requested dimensions, and last completed date window from its checkpoint.
  2. Read each bounded window and all its pages into a staging destination.
  3. 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.
  4. Advance the checkpoint only after the destination has accepted the complete window.
  5. Re-read a recent overlap window according to your source’s correction and freshness needs. Retain when each extract was observed.
Delivery reaches reporting through supported seller webhooks, source polling, and downstream aggregation. A successful API response is not a promise that a seller has reported every event or that the result is real-time. Review the reporting data flow for your sources. Buyer REST reporting is day-grain; an hourly job does not make that endpoint an hourly delivery report.

5. Choose CSV export or scheduled cloud delivery

For a CSV aggregate, use the current v2 REST reporting contract:
This example requests the credential’s available reporting scope. Add the documented advertiser or campaign filters when the job should cover a narrower scope. The response returns 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.

Use notifications to resume work

For a human operator, use the existing notification channels and inbox. Registered external buyer agents have separate machine-notification administration. Use the documented event and delivery contract for that connection, and re-read the referenced object before taking the next action. Do not use authored notification subscriptions as a scheduler for this pipeline: their schedule and condition triggers do not execute yet. A Slack incoming webhook is a notification destination, not a machine-readable export of all campaign changes. Your own scheduled poll or the supported Data Delivery schedule is the working path for recurring extraction.

Verify before connecting a dashboard

Test the consumer with empty data, an unavailable metric, mixed currencies, and more than one page. Prove that rerunning a completed window does not duplicate rows. Then read a known live campaign and compare its scope, period, and totals with the same report in Apostra. Fixture tests verify your consumer; this live comparison verifies access and the meaning of the selected report.