Skip to main content
Push notifications let publishers deliver task status updates to you directly, instead of requiring you to poll. You provide a webhook URL in the task request; the publisher POSTs status changes to that URL as the task progresses.

How it works

  1. A unique operation ID is generated per task invocation
  2. A webhook URL is built by substituting that ID (and other routing params) into a URL template
  3. push_notification_config is injected into the task request body with that URL and HMAC credentials
  4. The publisher POSTs webhook notifications to your URL as the task status changes
  5. Each notification echoes operation_id back in the payload so you can correlate it without parsing the URL
If you’re using the @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: The AdCP field name is always 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

Include push_notification_config as a task argument, merged with the rest of your task parameters:

A2A

For A2A, skill parameters stay in message.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:
  1. Generate a unique ID per task call
  2. Embed it in the webhook URL path
  3. The publisher echoes operation_id in the payload — no URL parsing needed
URL template pattern:
Example (client library handles this automatically):
The publisher’s webhook payload will include "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 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. Status changes that trigger webhooks:

Webhook payload formats

MCP

A2A

A2A sends a Task object (for final states) or TaskStatusUpdateEvent (for progress). The AdCP result data is in status.message.parts[].data:

Protocol comparison

Status-specific result data

Authentication

Bearer token

Simple token auth — the publisher sends your token in the Authorization header. Configuration:
Verifying on your server:
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:
Publisher sends two headers:
Signature algorithm: The signed message is {unix_timestamp}.{raw_json_body} — the Unix timestamp (in seconds), a dot, then the exact JSON bytes being sent in the HTTP body.
The 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):
Receiver implementation (verification):
:::caution Important: raw body verification Always verify the signature against the raw HTTP body bytes, not a re-serialized version of the parsed JSON. Different languages and libraries may produce different JSON serializations (key ordering, whitespace, number formatting). Capture the raw body before JSON parsing and use those exact bytes for HMAC verification. In Express, use the verify callback on express.json() to capture the raw body:
::: :::note Replay protection The 5-minute timestamp window prevents replay attacks. Publishers must use Unix timestamps in seconds (not ISO 8601) in the 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. Use task_id + status + timestamp to handle this:
Always implement polling as backup. Webhooks can fail due to network issues or server downtime. Use a slower poll interval when webhooks are configured (e.g., every 2 minutes instead of 30 seconds), and stop polling once you receive a terminal status via webhook.

Best practices

  1. 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
  2. Handle duplicates — use task_id + timestamp to skip already-processed or out-of-order events
  3. Verify signatures — always validate X-ADCP-Signature before processing
  4. Acknowledge immediately — return 200 before doing any heavy processing to avoid publisher timeouts and unnecessary retries
  5. Don’t rely on URL structure — use operation_id from the payload for routing, not URL parsing
  6. Use HMAC-SHA256 in production — Bearer tokens are simpler but don’t protect against payload tampering

Reporting webhooks

Reporting webhooks are separate from task status webhooks. They deliver periodic performance data for active media buys and are configured via reporting_webhook in create_media_buy, not via push_notification_config. See Task Reference for details on reporting_webhook.

Next steps