Skip to content

Conformance: synchronous completion must not emit a webhook by default #5389

Description

@bokelley

Summary

The spec is clear that a synchronous completion fires no webhook even when push_notification_config is present. Yet both reference SDKs default to the opposite — they emit a completion webhook on the synchronous-success arm when a push URL is supplied. Two independent implementations converging on a behavior the spec forbids is a signal worth a WG decision: either the SDKs should conform, or the spec should sanction an opt-in.

What the spec says today (clear)

docs/building/by-layer/L3/webhooks.mdx ("When webhooks fire"):

"Webhooks are sent for each status change after the initial response, as long as push_notification_config is in the request. If the task completes synchronously (initial response is already completed or failed), no webhook is sent — you already have the result."

Reinforced by:

  • static/schemas/source/media-buy/get-products-request.json & static/schemas/source/signals/get-signals-request.json — push is "meaningful only for brief/refine requests that enter the async lifecycle"; the MUST-deliver obligation is conditioned on "...AND the seller returns a Submitted envelope."
  • docs/building/by-layer/L3/async-operations.mdx, static/schemas/source/enums/task-status.json (submitted description), and the L0 MCP/A2A guides ("initial response is already terminal → no webhook is sent").

The rationale is explicit: on a synchronous completion the buyer already has the result inline, so a webhook would just duplicate the payload.

What both reference SDKs actually do (the divergence)

Both default to emitting a completion webhook on the synchronous arm when push_notification_config.url is present:

  • JS @adcp/sdkautoEmitCompletionWebhooks defaults to true (src/lib/server/decisioning/runtime/from-platform.ts). Its doc-comment justifies this on buyer-convenience grounds ("buyers passing the URL expect notification regardless of whether the seller routed the call sync vs HITL; v5 adopters routinely wired this manually") and synthesizes a task_id since sync responses allocate no registry task.
  • Python adcp-client-pythonauto_emit_completion_webhooks defaults to true in production serve(); handler._maybe_auto_emit_sync_completion fires the sync-completion webhook and its docstring states it "mirrors the JS-side routeIfHandoff logic."

Neither cites a spec clause — both are deliberate product choices that exceed the spec.

Why the SDK behavior is reasonable

A buyer that supplies push_notification_config is frequently fire-and-forget: send the request, expect a callback, don't necessarily branch on the synchronous HTTP body. "I gave you a URL, call me when it's done" is a clean, uniform mental model that frees the buyer from handling sync-vs-async differently. The fact that two independent implementations (and, per the JS note, many v5 adopters by hand) converged on it suggests real demand.

Why the spec currently forbids it

Avoids duplicating the inline result onto the webhook ("you already have the result"), and keeps the webhook channel semantically bound to the async (submitted) lifecycle.

Options for the WG

  • A. Keep the spec; SDKs conform. Sellers/SDKs MUST NOT emit a webhook on synchronous completion. SDK convenience emitters become explicit, off-by-default opt-ins documented as non-spec. (Requires both SDKs to flip their defaults.)
  • B. Spec sanctions an opt-in. Define a mechanism — e.g. a buyer-signalable flag, or "a seller MAY also deliver a terminal notification on synchronous completion when push_notification_config is present" — with operation_id correlation and explicit acknowledgement that this duplicates the inline result.
  • C. Spec requires it. push_notification_config present ⇒ the buyer is always notified via webhook, sync or async ("passed a URL ⇒ always called"). Strongest buyer guarantee; aligns the spec to current SDK defaults; cost is the duplicated payload.

Recommendation

Discuss B vs C. The convergence of both reference SDKs (and hand-rolled v5 adopters) on emitting points to a genuine buyer expectation that the spec doesn't currently meet. At minimum, the spec and the reference SDKs should not silently disagree — today a conformance test would mark both SDKs non-conformant by default.

Edge cases to settle if sanctioned (B/C)

  • operation_id correlation on a sync-completion webhook (buyer-supplied, echoed verbatim).
  • Double-delivery semantics: the buyer receives the result both inline and via webhook — is that explicitly fine, idempotent, de-dupable?
  • Interaction with idempotency replay (does a replayed sync response re-fire the webhook?).
  • Keep the existing prohibition intact: push_notification_config presence MUST NOT force a guaranteed-sync op (buying_mode/discovery_mode: "wholesale") onto the async/Submitted arm — a sync-completion webhook is a distinct thing from routing async and should be scoped accordingly.

References

  • docs/building/by-layer/L3/webhooks.mdx (≈154–169)
  • static/schemas/source/media-buy/get-products-request.json, static/schemas/source/signals/get-signals-request.json (push_notification_config descriptions)
  • docs/building/by-layer/L3/async-operations.mdx; static/schemas/source/enums/task-status.json; static/schemas/source/core/push-notification-config.json
  • JS: adcontextprotocol/adcp-client src/lib/server/decisioning/runtime/from-platform.ts (autoEmitCompletionWebhooks, default true)
  • Python: adcontextprotocol/adcp-client-python src/adcp/decisioning/handler.py (_maybe_auto_emit_sync_completion) + serve() default auto_emit_completion_webhooks=True

Surfaced while implementing async discovery parity (get_products/get_signals) in the Python SDK; the sync-vs-async webhook question generalizes to every task-capable operation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-triagedIssue has been triaged by the Claude Code triage routine. Remove to re-triage.compliance-suiterfcProtocol change — auto-adds to roadmap boardschemaJSON Schema source-of-truth: definitions, codegen artifacts, validation, hygienespec / protocol

    Type

    No type

    Projects

    Status
    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions