How it works
- A unique operation ID is generated per task invocation
- A webhook URL is built by substituting that ID (and other routing params) into a URL template
push_notification_configis injected into the task request body with that URL and HMAC credentials- The publisher POSTs webhook notifications to your URL as the task status changes
- Each notification echoes
operation_idback in the payload so you can correlate it without parsing the URL
@adcp/client library, this entire flow is handled automatically — configure webhookUrlTemplate and webhookSecret once on the client and push_notification_config is injected into every outgoing task call.
Naming: snake_case vs camelCase
This trips people up. There are two naming conventions in play:| Context | Field name | Example |
|---|---|---|
| MCP task arguments (AdCP JSON) | push_notification_config | { push_notification_config: { url: ... } } |
| A2A configuration object | pushNotificationConfig | configuration: { pushNotificationConfig: { url: ... } } |
push_notification_config (snake_case). It goes in the task request body alongside your other task parameters.
For A2A, the A2A protocol wraps it in a configuration envelope using camelCase — but the object’s contents are identical.
Adding push_notification_config to a request
MCP
Includepush_notification_config as a task argument, merged with the rest of your task parameters:
A2A
For A2A, skill parameters stay inmessage.parts[].data.parameters. The push notification config goes in the top-level configuration object:
Operation IDs and URL templates
Operation IDs let you route incoming webhooks to the right handler. The typical pattern:- Generate a unique ID per task call
- Embed it in the webhook URL path
- The publisher echoes
operation_idin the payload — no URL parsing needed
"operation_id": "cd51e063-2b79-4a6d-afac-ed7789c3a443", so your handler can route to the right pending operation without parsing the URL.
When webhooks fire
Webhooks are sent for each status change after the initial response, as long aspush_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.
Status changes that trigger webhooks:
| Status | Meaning |
|---|---|
working | Task is processing — may include progress info |
input-required | Waiting for human approval or clarification |
completed | Final result available |
failed | Task failed with error details |
canceled | Task was canceled |
Webhook payload formats
MCP
A2A
A2A sends aTask object (for final states) or TaskStatusUpdateEvent (for progress). For final states (completed, failed), AdCP result data is in .artifacts[0].parts[]. For interim states (working, input-required), data is in status.message.parts[].
Protocol comparison
| MCP | A2A | |
|---|---|---|
| Config field | push_notification_config (in task args) | configuration.pushNotificationConfig (separate from skill params) |
| Envelope | mcp-webhook-payload.json | Native Task / TaskStatusUpdateEvent |
| Result location | result field | .artifacts[0].parts[].data (final) / status.message.parts[].data (interim) |
| Data schemas | Identical AdCP schemas | Identical AdCP schemas |
Status-specific result data
| Status | result / data contains |
|---|---|
completed / failed | Full task response |
working | Progress: percentage, current_step, total_steps |
input-required | Reason and any validation errors |
submitted | Minimal acknowledgment |
Authentication
Bearer token
Simple token auth — the publisher sends your token in theAuthorization header.
Configuration:
HMAC-SHA256 (recommended for production)
The publisher signs each request with a shared secret and includes a timestamp for replay protection. You verify both the signature and the timestamp on receipt. Configuration:{unix_timestamp}.{raw_json_body} — the Unix timestamp (in seconds), a dot, then the exact JSON bytes being sent in the HTTP body.
rawBody must be the exact bytes sent on the wire. Implementations must sign the same serialization that goes in the HTTP body — signing a re-serialized or reformatted version of the payload will cause verification failures.
Publisher implementation (signing):
verify callback on express.json() to capture the raw body:
X-ADCP-Timestamp header. Receivers should reject requests where |current_time - timestamp| > 300 seconds.
:::
Reliability
Webhooks use at-least-once delivery — you may receive the same event more than once, and events may arrive out of order. Usetask_id + status + timestamp to handle this:
Best practices
- Always implement polling as backup — webhooks can fail; poll at a reduced interval (e.g. every 2 minutes) when webhooks are configured, and stop once you receive a terminal status
- Handle duplicates — use
task_id+timestampto skip already-processed or out-of-order events - Verify signatures — always validate
X-ADCP-Signaturebefore processing - Acknowledge immediately — return
200before doing any heavy processing to avoid publisher timeouts and unnecessary retries - Don’t rely on URL structure — use
operation_idfrom the payload for routing, not URL parsing - Use HMAC-SHA256 in production — Bearer tokens are simpler but don’t protect against payload tampering
Payload extraction
Webhook receivers need to detect the format and extract AdCP data. The buyer typically knows the format because it configured the transport, but defensive detection is useful for multi-format receivers.Format detection
| Signal | Format |
|---|---|
status is a string, task_id present | MCP |
status is an object with .state | A2A |
Extraction
MCP webhooks: Extract data from theresult field directly.
A2A webhooks: Use the A2A response extraction algorithm — final states extract from .artifacts[0].parts[] (last DataPart), interim states from status.message.parts[] (first DataPart).
Security requirements
- Content-Type validation: Publishers MUST send
application/json. Receivers MUST reject other types before HMAC verification. - Payload size limit: Receivers SHOULD enforce a 1MB limit. Reject before HMAC computation — computing HMAC over large payloads is a DoS vector. Return
413 Payload Too Large. - Deduplication:
task_id+timestampdedup provides defense-in-depth against replay attacks within the timestamp window. - Format detection: Auto-detection is a defensive fallback. Receivers SHOULD use the known format from their transport configuration (
knownFormatparameter) rather than relying solely on payload inspection. A compromised intermediary could craft an ambiguous payload that routes extraction to the wrong path.
Test vectors
Machine-readable test vectors are available at/static/test-vectors/webhook-payload-extraction.json. Client libraries SHOULD validate their format detection and extraction logic against these vectors.
Reporting webhooks
Reporting webhooks are separate from task status webhooks. They deliver periodic performance data for active media buys and are configured viareporting_webhook in create_media_buy, not via push_notification_config.
See Task Reference for details on reporting_webhook.
Next steps
- Task Lifecycle — status values and transitions
- Async Operations — handling long-running tasks
- Error Handling — webhook error patterns