Skip to main content

Creative Approval Webhook Architecture

Problem Statement

Creative sync to sales agents (e.g., Wonderstruck) is asynchronous:
  1. Push creativesync_creatives returns immediately with pending_approval status
  2. Manual review → Sales agent reviews creative (could take minutes, hours, or days)
  3. Status change → Creative approved, rejected, or changes requested
  4. Customer notification → How do we notify our customer of the status change?
Without webhooks, customers must poll creative_get repeatedly to check status, which is inefficient and delays response to rejections.

Architecture Overview

Two-Way Webhook Flow

┌─────────────┐         ┌──────────────┐         ┌─────────────┐         ┌──────────────┐
│  Customer   │         │ Activation   │         │   Sales     │         │   Customer   │
│   System    │────1───>│     API      │────2───>│   Agent     │────3───>│   Webhook    │
│             │         │              │         │(Wonderstruck│         │   Endpoint   │
└─────────────┘         └──────────────┘         └─────────────┘         └──────────────┘
                               ▲                                                 ▲
                               │                                                 │
                               └──────────────4──────────────────────────────────┘
Flow:
  1. Customer calls sync_creatives with their webhook URL
  2. We sync creative to sales agent, register webhook callback
  3. Sales agent reviews and approves/rejects
  4. Sales agent POSTs status to our webhook receiver
  5. We create internal notification
  6. We POST notification to customer’s webhook URL

Implementation Components

1. Outbound: Register Webhook with Sales Agent

When calling sync_creatives, include webhook registration:
// Option A: ADCP supports webhook_url parameter
POST /mcpsync_creatives
{
  creatives: [{...}],
  webhook_url: "https://api.agentic.scope3.com/webhooks/creative-status/{customer_id}/{creative_id}",
  webhook_events: ["creative.approved", "creative.rejected", "creative.changes_requested"],
  webhook_secret: "hmac-secret-for-verification"
}

// Option B: Separate webhook subscription call
POST /mcpsubscribe_creative_updates
{
  creative_id: "platform_creative_123",
  events: ["approved", "rejected", "changes_requested"],
  webhook_url: "https://api.agentic.scope3.com/webhooks/creative-status/{customer_id}/{creative_id}",
  secret: "hmac-secret"
}

// Option C: Polling fallback (if webhooks not supported)
// Poll every 5 minutes: GET /mcp → get_creative_status

2. Inbound: Receive Status Updates from Sales Agents

Endpoint: POST /webhooks/creative-status/:customerId/:creativeId
// Request from sales agent
{
  "creative_id": "wonderstruck_creative_456",           // Their ID
  "activation_creative_id": "creative_k0i02ddq5b",     // Our ID (if they tracked it)
  "status": "approved",                                 // approved | rejected | changes_requested
  "message": "Creative approved for display inventory",
  "reviewer": "john@wonderstruck.com",
  "reviewed_at": "2025-10-01T15:30:00Z",
  "platform_notes": "Looks great, approved for all formats",
  "signature": "hmac-sha256=abc123..."                  // HMAC verification
}

// Response
{
  "received": true,
  "notification_id": "notif_xyz789"
}
Security:
  • Verify HMAC signature using shared secret
  • Rate limit by IP and customer ID
  • Log all webhook deliveries for audit
Processing:
  1. Verify HMAC signature
  2. Look up creative in creative_sync_status table
  3. Update sync status and approval status
  4. Create Notification with type CREATIVE_APPROVED/REJECTED/CHANGES_REQUESTED
  5. Trigger customer webhook deliveries

3. Customer Webhook Subscriptions

Customers register their webhook endpoints to receive our notifications:
// Tool: webhook_subscribe
{
  "url": "https://customer-system.com/scope3/webhooks",
  "events": ["creative.approved", "creative.rejected", "creative.changes_requested"],
  "filters": {
    "brand_agent_ids": ["48"],          // Only for specific brand agents
    "campaign_ids": ["camp_123"]        // Only for specific campaigns
  },
  "authentication": {
    "type": "bearer",                   // bearer | basic | hmac
    "token": "customer-api-key"
  },
  "retry_policy": {
    "max_retries": 3,
    "backoff_seconds": [60, 300, 900]   // 1min, 5min, 15min
  }
}
Delivery to Customer:
POST https://customer-system.com/scope3/webhooks
Authorization: Bearer customer-api-key
X-Scope3-Signature: hmac-sha256=...
X-Scope3-Event-Type: creative.approved
X-Scope3-Event-ID: evt_abc123

{
  "event_id": "evt_abc123",
  "event_type": "creative.approved",
  "timestamp": "2025-10-01T15:30:00Z",
  "data": {
    "creative_id": "creative_k0i02ddq5b",
    "creative_name": "Wonderstruck Mushroom 300x250",
    "brand_agent_id": "48",
    "brand_agent_name": "Wonderstruck",
    "campaign_id": "camp_xyz",
    "campaign_name": "Wonderstruck Q4 Campaign",
    "sales_agent_id": "principal_8ac9e391",
    "sales_agent_name": "Wonderstruck",
    "approval_message": "Creative approved for display inventory",
    "approved_at": "2025-10-01T15:30:00Z",
    "platform_creative_id": "wonderstruck_creative_456",
    "action_url": "https://api.agentic.scope3.com/creatives/creative_k0i02ddq5b"
  }
}

Database Schema Updates

creative_sync_status Table

CREATE TABLE IF NOT EXISTS creative_sync_status (
  creative_id STRING NOT NULL,
  sales_agent_id STRING NOT NULL,
  customer_id INT64 NOT NULL,

  -- Sync status
  sync_status STRING NOT NULL,  -- pending | synced | failed | syncing
  sync_error STRING,
  synced_at TIMESTAMP,

  -- Approval workflow
  approval_status STRING,       -- pending | approved | rejected | changes_requested
  platform_creative_id STRING,  -- Sales agent's internal ID
  approval_message STRING,
  reviewer STRING,              -- Who approved/rejected
  reviewed_at TIMESTAMP,
  requested_changes STRING,     -- If changes_requested

  -- Webhook tracking
  webhook_registered BOOL DEFAULT FALSE,
  webhook_url STRING,
  webhook_secret STRING,        -- Encrypted

  -- Metadata
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),

  PRIMARY KEY (creative_id, sales_agent_id)
);

webhook_subscriptions Table (Already exists in types)

CREATE TABLE IF NOT EXISTS webhook_subscriptions (
  id STRING NOT NULL,
  customer_id INT64 NOT NULL,
  brand_agent_id STRING,

  -- Endpoint config
  url STRING NOT NULL,
  method STRING DEFAULT 'POST',  -- POST | PUT
  headers STRING,                -- JSON: {"Authorization": "Bearer ..."}

  -- Events and filters
  event_types ARRAY<STRING>,     -- ['creative.approved', 'creative.rejected', ...]
  filters STRING,                -- JSON: {"campaigns": [...], "brand_agents": [...]}

  -- Auth
  auth_type STRING,              -- bearer | basic | hmac
  auth_credentials STRING,       -- Encrypted

  -- Reliability
  retry_max INT64 DEFAULT 3,
  retry_backoff STRING,          -- JSON: [60, 300, 900]

  -- Health tracking
  status STRING DEFAULT 'active', -- active | paused | failing
  consecutive_failures INT64 DEFAULT 0,
  last_success_at TIMESTAMP,

  -- Metadata
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),

  PRIMARY KEY (id)
);

webhook_deliveries Table (Audit log)

CREATE TABLE IF NOT EXISTS webhook_deliveries (
  id STRING NOT NULL,
  subscription_id STRING NOT NULL,
  event_id STRING NOT NULL,

  -- Delivery details
  url STRING NOT NULL,
  event_type STRING NOT NULL,
  payload STRING NOT NULL,           -- JSON

  -- Result
  success BOOL NOT NULL,
  status_code INT64,
  response_body STRING,
  error_message STRING,
  retry_attempt INT64 DEFAULT 0,

  -- Timing
  delivered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
  duration_ms INT64,

  PRIMARY KEY (id)
);

Implementation Tasks

Phase 1: Inbound Webhook Receiver (High Priority)

  • Design webhook architecture
  • Create Express route: POST /webhooks/creative-status/:customerId/:creativeId
  • Implement HMAC signature verification
  • Update creative_sync_status on status changes
  • Create internal Notification records
  • Add rate limiting and IP validation
  • Add audit logging to webhook_deliveries

Phase 2: Customer Webhook Subscriptions

  • Create webhook_subscribe MCP tool
  • Create webhook_list MCP tool
  • Create webhook_unsubscribe MCP tool
  • Create webhook_test MCP tool (send test notification)
  • Implement webhook delivery service with retries
  • Add HMAC signature generation for customer webhooks

Phase 3: Sales Agent Integration

  • Check if ADCP spec supports webhook registration
  • Update sync_creatives to register webhooks with sales agents
  • Implement polling fallback for agents without webhook support
  • Add get_creative_status polling job (cron every 5 minutes)

Phase 4: Monitoring & Reliability

  • Add webhook delivery metrics (success rate, latency)
  • Implement auto-pause for repeatedly failing webhooks
  • Add customer notification preferences (email fallback)
  • Create admin dashboard for webhook health

Security Considerations

HMAC Signature Verification

// Incoming from sales agent
const computedSignature = crypto
  .createHmac("sha256", webhookSecret)
  .update(JSON.stringify(req.body))
  .digest("hex");

const receivedSignature = req.headers["x-webhook-signature"];
const isValid = crypto.timingSafeEqual(
  Buffer.from(computedSignature),
  Buffer.from(receivedSignature),
);

if (!isValid) {
  return res.status(401).json({ error: "Invalid signature" });
}

Outgoing to customers

// Outgoing to customer
const signature = crypto
  .createHmac("sha256", subscription.auth_credentials)
  .update(JSON.stringify(payload))
  .digest("hex");

headers["X-Scope3-Signature"] = `sha256=${signature}`;

Secrets Management

  • Store webhook secrets encrypted in BigQuery
  • Use Google Cloud Secret Manager for encryption keys
  • Rotate secrets periodically (monthly)
  • Provide secret rotation endpoint for customers

Alternatives Considered

1. Polling Only (No Webhooks)

Pros: Simpler implementation, no webhook receiver needed Cons: Higher latency, more API calls, customers must implement polling

2. Server-Sent Events (SSE)

Pros: Real-time updates, no customer webhook setup Cons: Requires persistent connections, complex state management

3. WebSockets

Pros: Bidirectional, real-time Cons: Overkill for infrequent status updates, complex infrastructure Decision: Webhooks + polling fallback provides best balance of simplicity, reliability, and real-time updates.

Testing Strategy

Unit Tests

  • HMAC signature verification
  • Webhook payload validation
  • Retry logic

Integration Tests

  • End-to-end creative sync → approval → notification flow
  • Webhook delivery with retries and failures
  • Customer webhook subscription CRUD

Load Tests

  • 1000 concurrent webhook deliveries
  • Sales agent webhook receiver under load

Rollout Plan

Week 1: Inbound Receiver

  • Build and deploy webhook receiver
  • Test with Wonderstruck sales agent
  • Document webhook format for sales agents

Week 2: Customer Subscriptions

  • Implement subscription MCP tools
  • Add webhook delivery service
  • Beta test with 3 customers

Week 3: Sales Agent Integration

  • Update sync_creatives with webhook registration
  • Implement polling fallback
  • Monitor webhook delivery success rates

Week 4: Production Rollout

  • Enable for all customers
  • Monitor and tune retry policies
  • Add customer-facing documentation

Success Metrics

  • Latency: Time from approval → customer notification < 5 seconds
  • Reliability: Webhook delivery success rate > 99%
  • Adoption: 80% of customers using webhooks within 3 months
  • Cost: Reduced polling API calls by 90%