Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt

Use this file to discover all available pages before exploring further.

Snapshot and log

Every state surface AdCP exposes has two faces: a snapshot read from a get_* task and a log of push events fired against a registered webhook URL. The snapshot says what is true now. The log says what fired, when, with what id. This page is the contract that keeps them coherent. You don’t need to read this page to call an AdCP task. You do need to read it to build a webhook receiver, to propose a new notification type, or to argue that a missing-event scenario is a spec gap rather than a buyer-side bug.

The two faces

Snapshot

The current truth, exposed on a read API:
  • get_media_buys returns each buy’s status, health, open impairments[], and webhook_activity[].
  • list_creatives returns each creative’s status.
  • sync_audiences (without changes) returns each audience’s current status.
  • get_event_source_health returns each source’s current assessment-status.
A snapshot is always re-readable. It carries no history — only what’s true at the moment of the read.

Log

A stream of push events fired to the buyer’s registered webhook URL:
  • Delivery report fires (notification_type: scheduled | final | delayed | adjusted).
  • Dependency impairment fires (notification_type: impairment).
  • Future event types are added the same way: a new notification-type value, a defined payload, the same delivery contract.
Each event carries a stable notification_id and corresponds to a change visible on the snapshot.

The five rules

These rules apply across every snapshot/log pair in the protocol. If you’re building a new notification type, your design must satisfy all five.

1. Two distinct ids: per-fire and per-state

Dedupe transport retries by idempotency_key. Correlate fires to state by notification_id. These are different ids on the same fire — receivers MUST track both.
  • idempotency_key — transport-layer, per delivery attempt. Issued by the seller for each fire. Receivers dedupe on this to suppress retries of the same logical fire. Defined in the webhooks transport contract.
  • notification_id — event-layer, per state event. Stable across re-emissions of the same logical event. For state-shaped events this equals the resource’s stable id (e.g., impairment_id is the notification_id for impairment events). Typed as a first-class envelope field via #4594 in 3.1; until that lands, surfaced in webhook payloads but not in the envelope schema.
The split is intentional. A receiver seeing the same idempotency_key twice is observing a transport retry — uninteresting, dedupe and move on. A receiver seeing the same notification_id twice under different idempotency_keys is observing a re-emission — signal. The seller is repeating itself, usually because the buyer’s receiver was unreachable for long enough that the seller wants to make sure the state was delivered. That’s a missed-events warning the receiver should not collapse. For state-shaped events (impairment, lifecycle), the per-state id is the resource id. For point-in-time data events (delivery report fires), there is no persistent state id — the per-fire idempotency_key is all there is. That asymmetry is honest about the limits of Rule 4 below.

2. Every push event corresponds to a snapshot delta

There is no webhook-only state. If a webhook fires with notification_type: impairment, the affected media buy’s impairments[] will show the impairment on the next read. If a delivery report fires, the next get_media_buy_delivery reflects the same reporting window. Push channels do not carry information unavailable from the read API. This rule rules out push events that exist solely as ephemeral signals — “you might want to know X” without a corresponding readable state. If you want to surface a suggestion that doesn’t change state, build a pull tool, not a webhook.

3. Push is at-least-once; the snapshot is authoritative

When push and snapshot disagree, the snapshot wins. A duplicate webhook fire (same notification_id) is the expected behavior under at-least-once delivery — buyer agents dedupe and continue. A stale webhook fire (the push reports a state that the snapshot no longer reflects, because the resource moved on) is also expected — buyer agents re-read the snapshot rather than acting on the push payload. This is why receivers MUST verify against the snapshot before taking irreversible action on a push.

4. Either path is complete

A buyer using webhooks reliably gets all the data. A buyer using only GET (no webhooks) gets the same data. The two paths are at parity in content and granularity; the buyer chooses based on latency, ergonomics, and receiver infrastructure. This rule has two halves:
  • For state events (impairment, lifecycle, status changes): GET returns current state. A buyer who missed a webhook calls get_* and reads the snapshot — recovery is lossless. ✅ Holds today.
  • For data-bearing events (delivery report fires, individual log events): GET SHOULD support the same windowing and granularity the webhook delivers. A seller that emits hourly reporting_webhook fires SHOULD support hourly windowed pulls on get_media_buy_delivery so a buyer can reconstruct identical data by polling. ⚠️ Partial today — get_media_buy_delivery supports date-range pulls and daily breakdowns, but not arbitrary per-window granularity matching webhook frequency. #4590 closes this for delivery reporting in 3.1; once it lands and is broadly adopted, SHOULD promotes to MUST for the granularities a seller declares in windowed_pull_granularities.
Until #4590 lands, sellers offering high-frequency reporting webhooks (hourly) are the one channel where missing a webhook can mean losing per-window detail until the next aggregate window closes. Buyers on those sellers SHOULD treat the webhook as primary; buyers polling daily-or-coarser are unaffected. Without two-paths-equal, AdCP becomes pub/sub for some channels and REST for others — buyers building against the contract have to know which model applies where. With it, both paths are equivalent: a buyer chooses webhooks for latency or polling for simplicity, and gets the same data either way.

5. Push events and log entries share an id space

A webhook delivery surfaced via webhook_activity[] references the same notification_id that the buyer received in the push body. A buyer can correlate “I received fire X” with “the seller’s log shows fire X” without bookkeeping across two namespaces. Likewise, an impairment_id referenced in impairments[] matches the notification_id of the push that announced it.

What this rules out

  • A push channel for suggestions that don’t change state. If “the seller wants you to know X” doesn’t correspond to a readable field, it’s not a snapshot/log event. Build a pull tool instead. (See the advisory epic.)
  • A replay tool that re-fires past webhooks. Snapshot reads are the replay. A replay tool is an operator-side debug feature; it’s not part of the buyer-facing protocol contract.
  • Per-event subscription filtering. Subscription is per-resource (or per-account, for resources that outlive a buy). A buyer who registers push_notification_config on a media buy receives every event type fired against that buy. Filtering at the receiver is fine; filtering at the protocol surface is out of scope.
  • A “did you receive my webhook?” confirmation step. Receivers acknowledge via HTTP 2xx; senders retry on non-2xx per the persistent webhook contract. Sellers do not poll buyers for receipt.

Where the surface doesn’t yet follow this

  • Delivery reports (scheduled / final / delayed / adjusted) predate this contract. Two related gaps close in 3.1:
    • Per-window data parityget_media_buy_delivery today returns aggregates and daily breakdowns but cannot reconstruct the per-window slices (hourly, monthly) that reporting_webhook delivers. #4590 adds windowed pulls so the GET path matches the webhook payload at the same granularity.
    • Per-fire transport log — even with per-window parity, buyers debugging webhook delivery want to see which fires hit their endpoint and when. The webhook_activity[] surface on get_media_buys (#4278) closes this for transport-layer observability.
  • Resource lifecycle webhooks beyond media-buy scope (creative state changes, audience suspensions outside a buy’s scope) are in progress under the creative-lifecycle webhooks RFC (#2261). Until those land, the snapshot half (a fresh list_creatives or sync_audiences call) is the only reliable signal for changes to resources not currently referenced by an active buy.

When you’d be right to push back

This section is non-normative. It describes when raising an exception is reasonable, not when one is sanctioned.
When a use case genuinely needs an event with no snapshot half — a high-frequency signal where polling cost dominates and recovery isn’t critical (e.g., a metrics stream). AdCP doesn’t have one of these today. If you’re proposing one, name it explicitly and argue why pull-via-snapshot doesn’t fit; reviewers will weigh that against the contract this page commits to.