Snapshot and log
AdCP state surfaces that pair a read API with push notifications have two faces: a snapshot read from a task and a log of 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 those paired surfaces coherent. A read API is not automatically a snapshot/log pair merely because its result contains state. 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_buysreturns each buy’sstatus,health, openimpairments[], and optionalwebhook_activity[].get_media_buy_deliveryreturns delivery data for the same reporting windows carried by delivery-report fires.list_creativesreturns each creative’s current lifecycle state, including soft-purge tombstones when requested.list_accountsreturns each account’s current status.- Wholesale
get_productsandget_signalsreads return versioned feed snapshots. get_adcp_capabilitiesreturns the agent’s current capability revision.
Log
A stream of push events fired to the buyer’s registered webhook URL:- Delivery report fires (
notification_type: scheduled | final | delayed | adjusted | window_update). - Dependency impairment fires (
notification_type: impairment). - Creative and account lifecycle fires.
- Wholesale product and signal feed-change fires.
- Agent capability-change fires.
- Future event types that adopt this contract are added the same way: a new
notification-typevalue, a defined payload, and a named repair read.
notification_id and names the resource or revision needed for a repair read. Point-in-time delivery-report events have no persistent event id and use only idempotency_key.
The five rules
These rules apply across every snapshot/log pair in the protocol. A new notification type can claim this contract only if its design satisfies all five.1. Separate transport, event, and resource identity
Dedupe transport retries byidempotency_key. Recognize a logical event by notification_id. Repair through the resource key defined by that notification type. These identifiers serve different purposes, and receivers MUST NOT substitute one for another.
idempotency_key— transport-layer, per logical fire. Issued by the seller and reused across delivery attempts of that fire. Receivers dedupe on this to suppress transport retries. A deliberate re-emission is a new fire with a newidempotency_key. Defined in the webhooks transport contract.notification_id— event-layer, per logical event. Stable across re-emissions of that event. It is not generally a resource id: creative and account notifications use transition ids, wholesale notifications useevent.event_id, and capability notifications use revision-event ids. Impairment is the special case wherenotification_idequalsimpairment_id. Typed at the envelope level onmcp-webhook-payload.json; eachnotification-typedescription defines its population. Point-in-time delivery events do not define it.- Repair key — the resource identifier or revision used on the authoritative read: for example,
creative_id,account_id, a media buy and reporting range, wholesale feed scope/version, orcapabilities_version. The event payload for every conforming pair MUST expose enough information to select that read.
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 point-in-time delivery events there is no persistent logical-event id — the per-fire idempotency_key, reporting range, and optional sequence_number are the available identity and recovery coordinates. That asymmetry is intentional.
2. Every push event points to readable authoritative state
There is no webhook-only authoritative current state within a snapshot/log pair. If animpairment webhook fires, a pair that declares both propagation surfaces exposes the current open impairment set through get_media_buys. If a delivery report fires inside the seller’s declared parity set, get_media_buy_delivery can reproduce the reporting data for that range and granularity.
State-change pushes MAY include transition metadata useful for routing or diagnostics—such as prior status, reason, initiator, changed paths, or feed-event metadata—that a current snapshot does not retain. That metadata is advisory history, not authoritative current state, and is not recoverable after a missed event in 3.2. Push-only events and data outside a declared pull-parity set are outside this contract and MUST be identified as such.
3. Push is at-least-once; the snapshot is authoritative
When push and snapshot disagree, the snapshot wins. A transport retry (sameidempotency_key) is expected under at-least-once delivery — buyer agents dedupe and continue. A re-emission (same notification_id, new idempotency_key) is a missed-events warning, so receivers reconcile the snapshot instead of collapsing it as a retry. 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. The read path provides the declared recovery guarantee
Snapshot/log pairs make one of two recovery guarantees. A notification type MUST state which guarantee applies; implementations and reviewers MUST NOT infer historical replay from current-state convergence.- State convergence — for impairment, lifecycle, feed, and capability-change events, the named read returns the complete authoritative state now. Missing a push may lose its transition-only metadata, but it cannot prevent the receiver from converging on the resource’s current state. Recovery is complete for current state, not for event history.
- Data parity — for delivery-report events,
get_media_buy_deliveryMUST reproduce the reporting data and windowing at every granularity the seller lists inreporting_capabilities.windowed_pull_granularities. The pull usestime_granularityplusinclude_window_breakdown: true;window_updaterecovery also usesmeasurement_windowandsupersedes_windowto replace the superseded slice.
["hourly", "daily"] MUST honor both hourly and daily windowed pulls. Pulls outside the set return UNSUPPORTED_GRANULARITY with error.details.supported_granularities. A seller MAY emit a higher-frequency webhook outside the declared set, but that fire does not claim snapshot/log recovery at that frequency.
5. Push events and log entries share an id space
When a read surface exposeswebhook_activity[], it reuses wire identifiers rather than minting a parallel namespace: idempotency_key MUST equal the push value, and a populated notification_id MUST equal the push value. Sellers SHOULD populate notification_id on newly recorded attempts for event types that define it. The field remains optional in 3.2 so retained activity created before event identity was persisted can still be returned for the required 30-day window. Point-in-time delivery events do not define notification_id and omit it.
Current conforming pairs
The table below is the durable catalog of pairs that claim this contract. “Guarantee” distinguishes current-state convergence from delivery-data parity; a pair does not acquire replay history merely because its push payload has a timestamp.
The following surfaces are deliberately outside the catalog:
- Webhook-only impairment propagation—
propagation_surfaces: ["webhook"]withoutsnapshot—has no repair read and does not claim this contract. - Delivery fires at a granularity absent from
windowed_pull_granularitieshave no promised data-parity recovery at that frequency. sync_audiencesand event-source health remain pull-only because 3.2 defines no matching lifecycle notification.- Hard
creative.purgedis reserved for compelled legal erasure and deliberately retains no read-side tombstone. It is a push-only exception, not a conformant snapshot/log pair; receivers that require durable evidence must persist the signed fire themselves.
Webhook activity log pattern
The transport half of Rule 5. Any AdCP resource that exposes a snapshot read API and has webhook fires associated with it MAY also surface awebhook_activity[] array on that read API — recent per-fire transport records, scoped to the calling principal, useful for buyer-side debugging when a fire didn’t land or a retry trail looks suspect. This section is the contract any resource adopting that surface MUST follow.
Canonical record shape
The record shape is fixed at/schemas/core/webhook-activity-record.json. Read schemas adopting this surface MUST $ref the canonical record rather than inline it — the shape is intentionally uniform across resources so a buyer’s debug tooling can consume webhook_activity[] from any read API without resource-specific parsing.
Each record carries idempotency_key (equals the payload’s idempotency_key per Rule 5 — no parallel delivery_id), optional notification_id, subscriber_id, fired_at, completed_at, notification_type, sequence_number, attempt (1-indexed; one record per attempt), status (success / failed / timeout / connection_error / pending), url (query string and fragment stripped, secret-shaped path segments redacted), http_status_code, response_time_ms, payload_size_bytes, and error_message (server-side classification only — never request/response bodies or headers).
For a fire whose payload defines notification_id, sellers SHOULD populate it on newly recorded attempts and MUST copy the payload value exactly when they do. Sellers MAY omit it on retained records created before 3.2 so adopting the field does not invalidate the existing 30-day activity window. Delivery-report fires do not define notification_id and omit it.
Request-field convention
Read schemas that surfacewebhook_activity[] MUST use the same two request-field names so callers can opt in uniformly across resources:
include_webhook_activity— boolean, defaultfalse. When true, the seller MAY return awebhook_activity[]array on each item (subject to the three-state presence semantics below).webhook_activity_limit— integer, range 1–200, default 50. Per-item cap on returned records, most-recent first.
Scoping (normative)
webhook_activity[] MUST be scoped to the calling principal. When multiple principals share visibility into the same resource via account-level access, each principal sees only fires targeting its own registered endpoint. This is the same scoping rule that applies to push delivery itself.
Retention (normative)
Sellers that surfacewebhook_activity[] MUST retain records for at least 30 days from each record’s completed_at. This applies uniformly to every terminal status — success, failed, timeout, and connection_error all populate completed_at (for timeout and connection_error it is the moment the seller declared the attempt terminal) and the 30-day clock runs from there. For records still in pending status (the attempt is in flight or queued for retry, completed_at is null), the clock runs from fired_at until the attempt terminates and then transitions to 30 days from completed_at — so a retry trail does not age out mid-flight just because the initial fire happened 29 days ago.
The 30-day floor is a hard contract — sellers unable to honor it MUST omit the field entirely (see three-state presence below) rather than return a shorter window. This gives buyers a single retention guarantee they can build debug tooling against, and gives sellers with thin storage a clean opt-out via the three-state semantics rather than forcing the spec to negotiate per-seller retention floors.
Three-state presence semantics
Sellers MUST NOT collapse these into a single state. Opting in via
include_webhook_activity: true does not override the seller’s intrinsic capability — a seller that cannot meet the retention floor returns omission regardless of the request.
Buyers diagnosing an unexpected omission have two readily observable signals to discriminate the cause without needing operator help: (1) their own push_notification_config registration state for the resource (rules out “no registered endpoint”) and (2) the seller’s capability declaration (rules out “capability surface excludes the channel”). When both check out, “seller does not persist fire history” is the remaining cause and no further protocol-side fix is available — escalate.
Record cardinality
One record per attempt. A successful first-attempt fire appears as a single record withattempt: 1. A 3-attempt retry trail (e.g., two failures then a success) appears as three records sharing idempotency_key — the trail is reconstructed by the buyer grouping records on that key.
Privacy
urlMUST have query string and fragment stripped, and high-entropy / token-shaped path segments SHOULD be further redacted.error_messageis a server-side classification string only — never request headers, response bodies, or buyer-endpoint stack traces.- Request and response bodies are out of scope for the basic surface. A future
include_webhook_payloadsextension may add them under stricter access controls, and would use the universal truncation sentinel at/schemas/core/truncation-sentinel.jsonwhen bodies exceed a configured cap.
Adoption checklist
Resources adoptingwebhook_activity[] MUST satisfy all of the following. The list is intentionally explicit so the “MUST” hooks are unambiguous; everything not on this list is at adopter discretion (e.g., per-resource cardinality tuning within the 1–200 range).
- Notification channel (prerequisite). Adoption requires a registered notification channel for the relevant fire types. Media buys use per-buy
push_notification_configandreporting_webhook; account-scoped resources usenotification_configs[]managed throughsync_accounts; agent capability changes usesync_agent_notification_configs. Without a channel there are no fires forwebhook_activity[]to log. Adopters MUST cite the specific channel in their call-site documentation. - Record shape. Item schema MUST
$ref/schemas/core/webhook-activity-record.json. Resource-specific cross-references (e.g., a parent-resource id when records are nested inside an account-level read) go on the canonical record’sextenvelope, not as top-level record fields. - Request fields. The opt-in field names MUST be
include_webhook_activity(boolean, defaultfalse) andwebhook_activity_limit(integer, 1–200, default 50). The 200 ceiling is the canonical cap; adopters MAY narrow the maximum on a per-resource basis but MUST NOT exceed 200 or rename the fields. - Scoping. MUST be calling-principal only, per § Scoping above.
- Retention floor. MUST honor the 30-day floor per § Retention above. The pivot (
completed_at, with carve-out forpending) is the same across resources. - Three-state presence cardinality. Omitted /
[]/ non-empty are the three states; adopters MUST NOT collapse them. - Capability gate. Adopters MUST document which resource-specific capability declaration gates the field (for media buys this is
capabilities.media_buy.propagation_surfacesincludingwebhook). The specific causes of the “field omitted” state ARE resource-specific and adopters MUST enumerate them in their call-site documentation; the cardinality and the rule that omission is not “no fires occurred” are universal. - Notification type registry. Adopters whose webhook fires carry notification types not in
/schemas/enums/notification-type.jsonMUST add those types to that shared enum rather than minting a parallel enum on the canonical record. The enum is the cross-resource registry.
Consumers and the dependency chain
Media-buy consumer
get_media_buys.media_buys[].webhook_activity[]uses the per-buypush_notification_config. Capability gate:capabilities.media_buy.propagation_surfacesMUST includewebhookfor the field to be surfaced on a buy. See get_media_buys § Webhook activity for the call-site documentation and the persistent webhook contract for the transport-side rules this surface debugs against.
Account-level consumers
Resources that outlive a single media buy register their push channel on the account, not on any one buy. The account-level surface isnotification_configs[] — an array of per-subscriber registrations carried on sync_accounts and echoed on list_accounts. Each entry filters by event_types[] so a subscriber only receives the types its endpoint handles, and multiple entries with distinct subscriber_ids fan a single event out to multiple endpoints (multi-subscriber composition).
- #2261 creative lifecycle, assignment, and indicator webhooks —
list_creatives.creatives[].webhook_activity[]adopts this pattern forcreative.status_changed,creative.purged,creative.assignment_changed, and assignment-levelindicators.changed. The notification channel is the account’snotification_configs[]set, registered viasync_accountsin either provisioning or settings-update mode. Supported event types and per-type coalescence windows are declared viaget_adcp_capabilities. The parent creative is unambiguous, soext.creative_idMAY be omitted on inner records. Assignment and indicator events are invalidations repaired completely throughget_media_buys;list_creativesis an optional bounded reverse projection, and neither its rows nor the webhook payload replaces the authoritative snapshot. See list_creatives § Webhook activity for the call-site documentation. - #5915 account status webhooks —
list_accounts.accounts[].webhook_activity[]uses the same pattern foraccount.status_changed. The webhook invalidates the account snapshot; buyers re-readlist_accountsfor the authoritative status, setup hints, billing terms, and authorization state. The payload intentionally omitssetup.urlso single-use setup links are fetched through the authenticated read path instead of fanned out to every subscriber. - Future account-scoped resources follow the same chain only after defining both halves: subscribe through
sync_accounts.accounts[].notification_configs[], name an authoritative repair read, and adoptwebhook_activity[]on that read when transport observability is supported.
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 indicator proposal.)
- 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 on per-buy push. A buyer who registers
push_notification_configon a media buy receives every event type fired against that buy. Filtering at the receiver is fine; filtering at the per-buy protocol surface is out of scope. Account-level subscriptions (notification_configs[]) are the exception — they filter byevent_typesat registration time because that surface is heterogeneous and an endpoint that handles only one resource family would otherwise receive events it cannot interpret. - 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.
Current limits
- Transition history is not a 3.2 replay surface. Current-state pairs recover the resource as it exists now; event-only reason, prior-state, initiator, and changed-field metadata can be lost when a push is missed.
- Delivery parity is capability-scoped.
get_media_buy_deliveryreproduces reporting data only at declaredwindowed_pull_granularities; buyers must persist higher-frequency webhook data when the frequency is outside that set. - Activity identity is migration-safe.
notification_idis optional onwebhook_activity[]records in 3.2 so sellers can return retained pre-adoption records. Strict presence for identity-bearing event types requires a major-version migration contract. - Audience lifecycle remains pull-only. A fresh
sync_audiencesis the reliable signal when an audience is not represented by an active media-buy impairment.
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.
Related
- Push notifications — the transport contract that this page sits on top of.
- Media buy lifecycle — applies snapshot/log to
status+health+impairments[].