Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"group": "Protocols",
"pages": [
"docs/protocols/getting-started",
"docs/protocols/core-concepts",
{
"group": "Choose Your Protocol",
"pages": [
Expand Down
18 changes: 9 additions & 9 deletions docs/media-buy/media-buys/optimization-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Publishers can proactively push reporting data to buyers through webhook notific

Configure reporting webhooks when creating a media buy using the `reporting_webhook` parameter:

**With Bearer token:**
```json
{
"buyer_ref": "campaign_2024",
Expand All @@ -82,14 +83,14 @@ Configure reporting webhooks when creating a media buy using the `reporting_webh
"url": "https://buyer.example.com/webhooks/reporting",
"authentication": {
"schemes": ["Bearer"],
"credentials": "secret_token_min_32_chars"
"credentials": "client-token-min-32-chars"
},
"reporting_frequency": "daily"
}
}
```

**Or with HMAC signature (recommended for production):**
**With HMAC-SHA256:**
```json
{
"buyer_ref": "campaign_2024",
Expand All @@ -98,19 +99,18 @@ Configure reporting webhooks when creating a media buy using the `reporting_webh
"url": "https://buyer.example.com/webhooks/reporting",
"authentication": {
"schemes": ["HMAC-SHA256"],
"credentials": "shared_secret_min_32_chars"
"credentials": "shared_per_tool_call_secret_min_32_chars"
},
"reporting_frequency": "daily"
}
}
```

**Security is Required:**
- `authentication` configuration is mandatory (minimum 32 characters)
- **Bearer tokens**: Simple, good for development (Authorization header)
- **HMAC-SHA256**: Production-recommended, prevents replay attacks (signature headers)
- Credentials exchanged out-of-band during publisher onboarding
- See [Webhook Security](/docs/protocols/core-concepts.mdx#security) for implementation details
**Authentication:**

Bearer: server echoes token in `Authorization` header. HMAC-SHA256: server signs payload with `X-ADCP-Signature` header.

For complete implementation details, see [Core Concepts - Webhook Authentication](/docs/protocols/core-concepts#webhook-authentication).

#### Supported Frequencies

Expand Down
8 changes: 4 additions & 4 deletions docs/media-buy/task-reference/create_media_buy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,8 @@ const result = await testAgent.createMediaBuy({
reporting_webhook: {
url: 'https://buyer.example.com/webhooks/reporting',
authentication: {
schemes: ['Bearer'],
credentials: 'secret_token_xyz_minimum_32_chars'
schemes: ['HMAC-SHA256'],
credentials: 'shared_per_tool_call_secret_min_32_chars'
},
reporting_frequency: 'daily',
requested_metrics: ['impressions', 'spend', 'video_completions']
Expand Down Expand Up @@ -518,8 +518,8 @@ async def create_with_reporting():
reporting_webhook={
'url': 'https://buyer.example.com/webhooks/reporting',
'authentication': {
'schemes': ['Bearer'],
'credentials': 'secret_token_xyz_minimum_32_chars'
'schemes': ['HMAC-SHA256'],
'credentials': generate_unique_secret('nike_q1_reporting')
},
'reporting_frequency': 'daily',
'requested_metrics': ['impressions', 'spend', 'video_completions']
Expand Down
4 changes: 2 additions & 2 deletions docs/media-buy/task-reference/get_products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,8 @@ POST /api/mcp/call_tool
"pushNotificationConfig": {
"url": "https://buyer.com/webhooks/adcp/get_products",
"authentication": {
"schemes": ["Bearer"],
"credentials": "secret_token_32_chars"
"schemes": ["HMAC-SHA256"],
"credentials": "shared_per_tool_call_secret_min_32_chars"
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions docs/protocols/a2a-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,20 @@ await a2a.send({
},
pushNotificationConfig: {
url: `https://buyer.com/webhooks/a2a/${taskType}/${operationId}`,
token: "client-validation-token", // Optional: for client-side validation
authentication: {
schemes: ["bearer"],
credentials: "shared_secret_32_chars"
schemes: ["Bearer"], // or ["HMAC-SHA256"]
credentials: "client-token-min-32-chars"
}
}
});
```

### Webhook Authentication

AdCP supports Bearer token (server echoes back in `Authorization` header) or HMAC-SHA256 (server signs payload with `X-ADCP-Signature` header).

For complete implementation details, see [Core Concepts - Webhook Authentication](/docs/protocols/core-concepts#webhook-authentication).

For webhook payload formats, protocol comparison, and detailed handling examples, see [Task Management - Push Notification Integration](/docs/protocols/task-management#push-notification-integration).

## SSE Streaming (A2A-Specific)
Expand Down
163 changes: 93 additions & 70 deletions docs/protocols/core-concepts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -516,63 +516,96 @@ AdCP webhooks use **at-least-once delivery** semantics with the following charac

### Security

#### Webhook Authentication (Required)
#### Webhook Authentication

AdCP supports two authentication methods:

**AdCP adopts A2A's PushNotificationConfig structure** for webhook configuration. This provides a standard, flexible authentication model that supports multiple security schemes.
**1. Bearer Token (Simple)**

Client provides token, server echoes back in `Authorization` header. Token format is client's choice (JWT, random string, etc.).

**Configuration Structure (A2A-Compatible):**
```json
{
"push_notification_config": {
"url": "https://buyer.example.com/webhooks/adcp",
"url": "https://buyer.example.com/webhooks/adcp/nike_q1_2025",
"authentication": {
"schemes": ["Bearer"],
"credentials": "secret_token_min_32_chars"
"credentials": "client-provided-token-min-32-chars"
}
}
}
```

**Supported Authentication Schemes:**

1. **Bearer Token (Simple, Recommended for Development)**
```json
{
"authentication": {
"schemes": ["Bearer"],
"credentials": "secret_token_32_chars"
}
}
```

2. **HMAC Signature (Enterprise, Recommended for Production)**
```json
{
"authentication": {
"schemes": ["HMAC-SHA256"],
"credentials": "shared_secret_32_chars"
}
}
```

**Publisher Implementation (Bearer):**
Server sends:
```http
POST /webhooks/adcp/nike_q1_2025
Authorization: Bearer client-provided-token-min-32-chars
Content-Type: application/json

{ "task_id": "...", "status": "completed", ... }
```

Client validates:
```javascript
const config = pushNotificationConfig;
const scheme = config.authentication.schemes[0];
app.post('/webhooks/adcp/:operation_id', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing Authorization header' });
}

if (scheme === 'Bearer') {
await axios.post(config.url, payload, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.authentication.credentials}`
const token = authHeader.substring(7);
const expectedToken = await getTokenForOperation(req.params.operation_id);

if (token !== expectedToken) {
return res.status(401).json({ error: 'Invalid token' });
}

await processWebhook(req.params.operation_id, req.body);
res.status(200).json({ status: 'processed' });
});
```

**2. HMAC-SHA256 Signature (Advanced)**

Client provides secret, server signs payload.

```json
{
"push_notification_config": {
"url": "https://buyer.example.com/webhooks/adcp/nike_q1_2025",
"authentication": {
"schemes": ["HMAC-SHA256"],
"credentials": "unique-secret-for-this-operation-min-32-chars"
}
});
}
}
```

#### HMAC Signature Flow

**1. Client generates unique secret:**
```javascript
const secret = 'shared_per_tool_call_secret_min_32_chars';
```

**2. Client provides secret in push_notification_config:**
```json
{
"push_notification_config": {
"url": "https://buyer.com/webhooks/adcp/nike_q1_2025",
"authentication": {
"schemes": ["HMAC-SHA256"],
"credentials": "shared_per_tool_call_secret_min_32_chars"
}
}
}
```

**Publisher Implementation (HMAC-SHA256):**
**3. Server signs payload:**
```javascript
if (scheme === 'HMAC-SHA256') {
const crypto = require('crypto');

function sendWebhook(config, payload) {
const timestamp = new Date().toISOString();
const signature = crypto
.createHmac('sha256', config.authentication.credentials)
Expand All @@ -589,62 +622,52 @@ if (scheme === 'HMAC-SHA256') {
}
```

**Buyer Implementation (Bearer):**
**4. Client verifies signature:**
```javascript
app.post('/webhooks/adcp', async (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing Authorization header' });
}

const token = authHeader.substring(7);
if (token !== process.env.ADCP_WEBHOOK_TOKEN) {
return res.status(401).json({ error: 'Invalid token' });
}

await processWebhook(req.body);
res.status(200).json({ status: 'processed' });
});
```

**Buyer Implementation (HMAC-SHA256):**
```javascript
app.post('/webhooks/adcp', async (req, res) => {
app.post('/webhooks/adcp/:operation_id', async (req, res) => {
const signature = req.headers['x-adcp-signature'];
const timestamp = req.headers['x-adcp-timestamp'];

if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}

// Reject old webhooks (prevent replay attacks)
const eventTime = new Date(timestamp);
if (Date.now() - eventTime > 5 * 60 * 1000) {
const age = Date.now() - eventTime.getTime();
if (age > 5 * 60 * 1000) {
return res.status(401).json({ error: 'Webhook too old' });
}

// Verify signature
const secret = await getSecretForOperation(req.params.operation_id);
const expectedSig = crypto
.createHmac('sha256', process.env.ADCP_WEBHOOK_SECRET)
.createHmac('sha256', secret)
.update(timestamp + JSON.stringify(req.body))
.digest('hex');

if (signature !== `sha256=${expectedSig}`) {
return res.status(401).json({ error: 'Invalid signature' });
}

await processWebhook(req.body);
await processWebhook(req.params.operation_id, req.body);
res.status(200).json({ status: 'processed' });
});
```

**Authentication Best Practices:**
- **Bearer tokens**: Simple, good for development and testing
- **HMAC signatures**: Prevents replay attacks, recommended for production
- Credentials exchanged out-of-band (during publisher onboarding)
- Minimum 32 characters for all credentials
- Store securely (environment variables, secret management)
- Support credential rotation (accept old and new during transition)
#### Security Requirements

**Clients:**
- Generate unique credentials per webhook configuration
- Use minimum 32 characters for credentials
- Verify signature before processing webhook
- Validate timestamp (reject >5 minutes old)
- Rotate credentials periodically

**Publishers:**
- Sign every webhook with client-provided credentials
- Include timestamp in signature calculation
- Send signature in `X-ADCP-Signature` header
- Send timestamp in `X-ADCP-Timestamp` header
- Use HTTPS for all webhook deliveries

### Retry and Circuit Breaker Patterns

Expand Down
10 changes: 8 additions & 2 deletions docs/protocols/mcp-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,8 @@ const response = await session.call('create_media_buy',
pushNotificationConfig: {
url: `https://buyer.com/webhooks/adcp/${taskType}/${operationId}`,
authentication: {
schemes: ["HMAC-SHA256"], // or ["bearer"] for simple auth
credentials: "shared_secret_32_chars"
schemes: ["Bearer"], // or ["HMAC-SHA256"]
credentials: "client-token-min-32-chars"
}
}
}
Expand All @@ -286,6 +286,12 @@ if (response.status === 'submitted') {
}
```

#### Webhook Authentication

AdCP supports Bearer token (server echoes back in `Authorization` header) or HMAC-SHA256 (server signs payload with `X-ADCP-Signature` header).

For complete implementation details, see [Core Concepts - Webhook Authentication](/docs/protocols/core-concepts#webhook-authentication).

**Webhook POST format:**
```json
{
Expand Down
14 changes: 7 additions & 7 deletions docs/protocols/protocol-comparison.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ const updates = await session.call('tasks/get', {
include_result: true
});

// Optional: Configure webhook at protocol level (A2A-compatible structure)
// Optional: Configure webhook (Bearer or HMAC-SHA256)
const response = await session.call('create_media_buy', params, {
push_notification_config: {
url: "https://buyer.com/webhooks",
authentication: {
schemes: ["HMAC-SHA256"], // or ["Bearer"]
credentials: "shared_secret_32_chars"
schemes: ["Bearer"], // or ["HMAC-SHA256"]
credentials: "client-token-min-32-chars"
}
}
});
Expand All @@ -140,14 +140,14 @@ events.onmessage = (event) => {
console.log(`Status: ${update.status}, Message: ${update.message}`);
};

// Native webhook support
// Native webhook support (Bearer or HMAC-SHA256)
await a2a.send({
message: { /* skill invocation */ },
push_notification_config: {
webhook_url: "https://buyer.com/webhooks",
url: "https://buyer.com/webhooks",
authentication: {
schemes: ["Bearer"],
credentials: "secret_token_min_32_chars"
schemes: ["Bearer"], // or ["HMAC-SHA256"]
credentials: "client-token-min-32-chars"
}
}
});
Expand Down
Loading
Loading