Skip to main content
Deprecated. This page documents the v2 storefront API, which is being retired for external callers. Use Read seller-side media buys in V3 instead.
Three read-only surfaces answer the operating questions “what buys do I have, where is this one stuck, and what needs attention” — without a ticket and without platform support looking at a database. Each is available as a REST endpoint and as an in-chat surface (ask Apostra, or open it from the rail’s Operate section). All examples use the storefront base URL:
Authenticate every request with Authorization: Bearer $SCOPE3_API_KEY.

Recover a failed GAM order safely

If GAM created an order but line-item trafficking failed, Pending Operations shows a separate cleanup task. Apostra uses ESA’s structured cleanup result; it never interprets vendor exception text or exposes GAM credentials.
  • If ArchiveOrders is missing, grant it in GAM and re-check, or archive the order manually.
  • If authentication expired, reconnect the ad-server connection before re-checking.
  • Review and archive appears when cleanup is verified retryable. Permission and authentication failures instead offer a confirmation-gated re-check, and archive proceeds only if the repeated safety check passes.
  • Unknown or unsafe orders require GAM review and cannot be archived from Apostra.
Only storefront account admins can resolve these tasks. Archiving requires explicit confirmation naming the GAM order, and ESA repeats every safety check immediately before archiving. An already-archived response completes the task. Marking an order manually cleaned requires the matching order ID and an audit note; it is recorded distinctly and never claims ESA archived it.

Media buys — every buy on the storefront

GET /media-buys returns every buy forwarded across connected sources plus buys managed directly by an ad-server source. The API retains kind: routed | esa as legacy wire values; those values describe implementation provenance, not campaign mode or settlement. Filter by status, buyerCustomerId, sourceId, or flight-start window. Pass sellerAccountId to list only the buys routed through one company-owned seller account, the account a buyer company transacts through. It is the sellerAccountId that the seller account reads return. An account that is not one of your storefront’s, or does not exist, is refused with the same ACCESS_DENIED answer, so an id never confirms another storefront’s account. A buy matches only when its route is scoped to that account. Buys routed under a legacy account relationship, buys with no account scope, and buys managed directly by an ad-server source never match. Pass either sellerAccountId or accountRelationshipId, not both. Pass activeOnly=true to drop buys that are finished and cannot need anyone: completed, canceled, rejected. It defaults to false, so the list is your whole inventory. Set it on any “what needs me now” view, because the list is newest-first and a buy that settled yesterday otherwise outranks an older one still running. Leave it off when you filter by one of those three statuses, or the list comes back empty. The default sort is urgency: buys still waiting on someone whose flight starts within 48 hours (or has already started) come first, ordered by flight start; everything else follows newest-first. Pass sort=attention to get the first page ordered by what needs you: money the flight will not deliver, then work waiting on you (including a delivery problem you have to act on: a source that cannot report, a running flight nothing has reported for, or spend running ahead of the flight), then a goal being missed, then everything else. That ranking depends on delivery and goal verdicts that can only be judged once a page is loaded, so it is accepted at skip=0 only and the response says attentionRanked. It ranks a page, never your whole storefront — page past the first and you are back in the stable urgency order. Your storefront home opens in this order, and so does the Media buys entry in the navigation — they are the same page over the same buys, and ordering them differently by how you arrived would rearrange the list with nothing to explain it. Each row carries:

What needs you

Each buy says what needs you, in three families that answer different questions. They are deliberately kept apart: “spend 200 a day at 80% viewable” is two answers, and a single score would tell you to act without saying what to act on. level is the platform’s status colour language:
  • critical (red) — the flight is running and the promise is not being kept. Red is earned by the flight having started: a buy that has not started is never red, however far behind its budget it looks, because nothing was promised yet.
  • attention (yellow) — worth acting on, promise intact: a task waiting on you, reporting delayed, a source that cannot report.
  • pending (neutral) — not failure. The flight has not started, or the evidence does not support a verdict yet.
A withheld verdict is never red. Too few observations, a metric your source has not reported, no flight recorded — all read “not enough data yet”. If red appeared for things that are merely unknown, red would stop meaning stop.

Delivery and pacing

Every buy on the page carries delivery, whether or not you filtered the list to one buyer relationship. Delivery arrives in one of two fields, and never both. delivery is what the storefront reporting pipeline reported for a routed buy, and it always names the reporting day it covers. adServerDelivery is the cumulative total your ad server holds for a buy it manages, and it never names a day — an ad server states a total without saying which day it runs through, and we will not stamp one on. The fields below describe both except where a row says otherwise: lastDeliveryDate and sourcesReported/sourcesRouted are on delivery alone, because a day and a routing are things only a routed buy has.
An ad server that has not granted us reporting access cannot report delivery no matter how long you wait. That buy reads reporting.state: "blocked" with a detail naming what to fix, rather than sitting on “awaiting first report” forever. Buys managed by your ad server carry no clicks, views or completedViews yet — the ad-server list does not report them, and an ad server that reports only one of impressions and spend reports neither here: a spend of zero beside real impressions is not a measurement, and pacing it would invent a confident verdict.

The buyer’s goal

You see the same goal verdict the buyer sees, judged by the same code over the delivery your own sources reported. It is on every row of the list and on the per-buy timeline. You see it only from what the buyer told you: the commitment recorded against the buy when it was booked, and the optimization_goals on the AdCP request the buyer sent you. Nothing here reads the buyer’s campaign record. A buy whose goal was never disclosed to you carries goal: null.

A goal a measurement vendor counts

A buyer can set a goal that a measurement vendor counts, such as store visits measured by a third party. You cannot see the vendor’s numbers, so your own delivery cannot judge it. Until the buyer’s side tells you how the goal is doing, the goal reads metric_not_reported. On a storefront this platform hosts, the buyer’s side tells you with an AdCP provide_performance_feedback assertion, which your storefront accepts and keeps. The goal then shows the latest score the buyer reported for that buy and metric: You receive the score only. You never receive the vendor’s values, the impressions behind the rate, or which measurement source the buyer uses. The buyer scores a goal only when it has a rate target (a threshold_rate), and only for the buy you are the one seller of. A cost target, a buy split across sellers and a campaign-wide vendor result are never scored. Each new score replaces the earlier one on the goal. A buy that has no score yet, or whose buyer is not sending scores, stays metric_not_reported.
optimizedBy.actor === "seller" is the case to watch. It means the ad server is not working towards the buyer’s goal — either it has no optimizer, or your vendor has not granted us the access optimization needs. Hitting the goal is your ad operations’ job, and this row is the only place that says so.

Per-buy timeline — what was sent, when, and where it is stuck

GET /media-buys/{mediaBuyId}/timeline is the seller-scoped projection of the buy’s exchange record. Because routed media-buy ids are buyer-scoped, pass the row’s buyerCustomerId query parameter when it is available; the Media Buys Page does this automatically. Id-only requests remain supported for callers whose ids are unambiguous. It also carries the same goal block the list row does, so a drill-in answers “is this buy hitting what the buyer asked for” alongside “where is it stuck”. Stages follow the exchange lifecycle, failure branches included:
Accepted means the source accepted the transaction boundary. It does not mean trafficked or delivered. Creative processing, modular trafficking, and other seller work can remain pending after acceptance; the timeline and nextAction name that remaining boundary honestly. Update exchanges reuse the same stages. Inbound source webhooks appear as evidence inside stages, never as a stage of their own — many sources never send webhooks, so their absence means nothing. The screened stage is reserved for the acceptance-policy pre-screen record and is not emitted today; do not wait for it. What you can see, and what you cannot. The forwarded payload is yours to inspect — you are the counterparty to that message — minus platform-internal fields: webhook/push-notification configuration and any signing or credential material are removed server-side. Platform-internal diagnostics (resolution cache internals, worker polling state, cross-tenant audit entries) never appear; failures surface as structured error codes with a recovery class instead. A payload shown as {"pruned": true} means retention removed the stored bytes (payload bodies are kept for 90 days; the slim event record is kept indefinitely). Each source leg carries a trafficker-grade summary of the request (flight, budget, packages, targeting dimensions) with the raw JSON behind a disclosure.

Whose reference to quote to whom

Every leg carries two references, and they are for different counterparties: While a source is still moderating (no upstream buy exists yet), their task id is the handle; once accepted, their media-buy id is.

Pending operations — everything waiting on someone

In Apostra, open this Page from Approvals & operations under Operate in Storefront. The Page shows the union of work in flight, grouped; a group appears only when it is non-empty:
  • approvals — media buys (creates and updates) waiting on your review.
  • creativeReviews — creatives waiting on your review. Each item carries the exact reviewRef used by the Page action, so repeated versions of one buyer creative never open or decide a different review.
  • failedForwards — approved buys that could not be sent to their source, grouped by structured error code so a source outage reads as one row, not N. Each group and item carries its gated action (below).
  • awaitingSource — buys a source accepted asynchronously and is still moderating, each with “waiting since” and the source’s task id.
  • updatesAwaitingSource — changes to live buys that a source accepted with a task and has not applied yet.
  • creativesAwaitingSource — approved creatives not yet accepted at a source: the source is reviewing them, Apostra is still delivering them, or they wait for their media buy to be accepted there. mediaBuyId is null for a creative-library delivery.
  • manualSourceWork — modular booking, creative-sync, and final-reporting tasks, grouped by the same buyerCustomerId + mediaBuyId + sourceId. Every group and item repeats buyerCustomerId; every item carries its exact workItemId, seller owner, actionable/blocked state, and a typed deep-link to that exact source work item.
  • sourceDegradations — seller-owned source-health diagnoses: an ad-server or sales-agent source you operate that is degraded, each with its severity (blocking, attention, or advisory) and a one-line summary. Only diagnoses you own appear — an issue Apostra or a vendor must fix never shows up as your task. Each row links to the source diagnostics surface scoped to that source.
  • gamCleanups — failed GAM order cleanups that require an operator decision. The Page offers automatic archive only when the server marks the cleanup safe; recording a manual cleanup requires a note.
Every item in approvals, creativeReviews, awaitingSource, updatesAwaitingSource, creativesAwaitingSource, and manualSourceWork carries waitingOn: who it is waiting on, since when, and the exact thing being waited for. It is the same answer the source health object gives in sourceHealth.operations, with the same meanings:
  • waitingOn.party is seller (you must approve, review, or do manual work), operator (the party that runs the Agent behind the source holds a task it has not finished), buyer (the buyer must attach creatives), or apostra (Apostra must make the next move, such as retrying a forward or delivering a creative). Behind Apostra’s own managed sales agent there is no separate operator, so a buy it holds waits on your own review in your ad server and reads seller.
  • A buy the source has accepted but is holding for creatives waits on whoever can move it. It waits on the buyer only while the buyer has attached no creatives. Once creatives are attached it waits on apostra until they have reached the source, then on the source’s own review, which reads seller. When the buyer’s own buy has ended (completed, archived or canceled, with its flight end date past), it is no longer listed: nobody can still supply what the source wanted.
  • waitingOn.since is when the wait on that party began. Age never decides whether something is pending: an item is listed from its first minute.
  • waitingOn.target names what is being waited for as a kind and id: an approval (the media buy id), a creative_review, a manual work_item, the source’s task id, or the creative still to be delivered. It is null when there is no single thing to name.
A failed forward carries waitingOn too: apostra while Apostra is still retrying it, and null once it has stopped (terminalized), because the failure is settled and the next step is the group’s recovery action. The same Page works in Apostra, Claude, ChatGPT, and other MCP-app hosts. Its buttons open the exact portable Approvals, timeline, retry, or source-diagnostics surface directly—there is no generated chat prompt or second assistant turn. GAM cleanup is a two-step human confirmation inside the Page and uses an app-only commit that is not exposed to the model. Manual modular-source work is bounded to 200 tasks per response. Its envelope keeps full-backlog count and mediaBuyCount, and returns offset, returnedCount, truncated, and nextOffset. The Pending Operations Page continues from nextOffset with Load more, appending tasks without deduplicating different buyers that reuse the same media-buy id. If completed tasks make a requested offset fall past the live backlog, the server rebases offset to 0; clients should replace their loaded manual-work rows when the returned offset moves backward. The Page handles that reset.

Recovery classes: why some failures have no retry

Every forwarding failure carries a recovery class, and the class — not the operator’s optimism — decides which action is offered: A transient failure whose retry window has lapsed is treated as terminalized and also becomes escalate-only. A refusal that sending the same request again cannot change is structural even when your agent marks it transient or gives no recovery. Examples are a billing refusal such as BILLING_NOT_PERMITTED_FOR_AGENT, ACCOUNT_SUSPENDED and AGENT_BLOCKED. The platform stops sending it instead of retrying for 6 hours. If your agent marks a refusal correctable, it stays correctable. Credential errors such as AUTH_INVALID stay retryable, because reconnecting can fix them.
If your source consistently produces transient timeout failures on create_media_buy or update_media_buy — visible in source diagnostics and as persistent failedForwards here — the long-term fix is to implement AdCP asynchronous acceptance on your sales agent. Return a submitted response (with a stable task_id) immediately when the platform’s request arrives, complete the work out-of-band, then POST the result to the push_notification_config webhook URL included in the original request. This decouples your processing time from the platform’s call timeout and eliminates the failure class entirely.See the AdCP async operations reference for the protocol and payload details. Contact your operator or Apostra support to discuss implementation.

Transition notifications

You do not have to keep this surface open: a forward failure on an approved buy and a source-moderation wait that ages past its deadline each push one notification to your storefront’s notification stream (in-app, email, Slack — per your delivery settings). These fire on the transition only, never repeatedly, and carry the error code and recovery class.

Task reference

List media buys

GET /media-buys — every buy across the storefront and connected sources, urgency-sorted

Get a buy's timeline

GET /media-buys/{mediaBuyId}/timeline — the seller-scoped exchange record