> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ensemble.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive ensemble triggers via webhooks and send event notifications

## Overview

Conductor supports webhooks in two directions:

1. **Inbound Webhooks** - Trigger ensembles by calling a webhook URL
2. **Outbound Notifications** - Send events from ensembles to your webhooks

## Inbound Webhooks

Expose ensembles as webhook endpoints that can be triggered by external services.

<Note>
  **You own your webhook paths.** You can define any path for your webhooks (e.g., `/github/events`, `/stripe/payments`, `/my-custom-path`). We recommend using `/webhooks/*` paths for clarity and consistency (e.g., `/webhooks/github`, `/webhooks/stripe`).
</Note>

### Configure Inbound Webhook

```yaml theme={null}
name: github-pr-review

trigger:
  - type: webhook
    path: /webhooks/github/pr-review  # Recommended: /webhooks/* prefix
    methods: [POST]
    public: false
    auth:
      type: bearer
      secret: ${env.GITHUB_WEBHOOK_SECRET}

flow:
  - agent: review-pr

agents:
  - name: review-pr
    operation: api
    config:
      # ... analyze PR

outputs:
  analysis: ${review-pr.output}
```

### Trigger Inbound Webhook

```bash theme={null}
curl -X POST https://your-worker.workers.dev/webhooks/github/pr-review \
  -H "Authorization: Bearer ${GITHUB_WEBHOOK_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "opened",
    "pull_request": {
      "number": 123,
      "title": "Add new feature"
    }
  }'
```

Response:

```json theme={null}
{
  "executionId": "exec-abc123...",
  "status": "completed",
  "output": {
    "analysis": "LGTM",
    "score": 95
  },
  "metrics": {
    "totalDuration": 1234,
    "agents": 3
  }
}
```

### Authentication Options

**Bearer Token**:

```yaml theme={null}
trigger:
  - type: webhook
    path: /webhooks/secure-endpoint
    auth:
      type: bearer
      secret: ${env.WEBHOOK_TOKEN}
```

**HMAC Signature**:

```yaml theme={null}
trigger:
  - type: webhook
    path: /webhooks/github
    auth:
      type: signature
      secret: ${env.GITHUB_SECRET}
```

When using HMAC signature auth, the sender must include these headers:

```http theme={null}
X-Webhook-Signature: sha256=abc123def456...
X-Webhook-Timestamp: 1705315200
```

The signature is computed as:

```javascript theme={null}
const payload = `${timestamp}.${body}`;
const signature = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(payload)
  .digest('hex');
```

**OAuth** (coming soon):

```yaml theme={null}
trigger:
  - type: webhook
    path: /webhooks/oauth-endpoint
    auth:
      type: oauth
      issuer: https://auth.example.com
      audience: https://api.example.com
```

**Basic Authentication**:

```yaml theme={null}
trigger:
  - type: webhook
    path: /webhooks/basic-auth
    auth:
      type: basic
      secret: ${env.BASIC_AUTH_CREDENTIALS}  # Format: username:password
```

**Public (No Auth)**:

```yaml theme={null}
trigger:
  - type: webhook
    path: /webhooks/public
    public: true  # Explicitly allow anonymous access
```

## Outbound Notifications

Send notifications when events occur during ensemble execution.

### Configure Notifications

```yaml theme={null}
name: user-onboarding

notifications:
  # Webhook notification
  - type: webhook
    url: https://your-api.example.com/webhooks/conductor
    events:
      - execution.started
      - execution.completed
      - execution.failed
    secret: ${env.WEBHOOK_SECRET}
    retries: 3
    timeout: 5000

  # Email notification
  - type: email
    to:
      - devops@example.com
      - alerts@example.com
    from: conductor@example.com
    subject: "Ensemble ${ensemble.name}: ${event}"
    events:
      - execution.failed
      - execution.timeout

flow:
  - agent: onboard-user

agents:
  - name: onboard-user
    operation: think
    config:
      # ... your agent config

outputs:
  userId: ${onboard-user.output.userId}
```

### Notification Events

#### execution.started

Triggered when execution begins.

```json theme={null}
{
  "event": "execution.started",
  "timestamp": "2024-01-15T10:30:00Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "input": {
      "email": "alice@example.com"
    }
  }
}
```

#### execution.completed

Triggered when execution completes successfully.

```json theme={null}
{
  "event": "execution.completed",
  "timestamp": "2024-01-15T10:30:15Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "status": "completed",
    "output": {
      "userId": "user_xyz789",
      "created": true
    },
    "duration": 1234
  }
}
```

#### execution.failed

Triggered when execution fails.

```json theme={null}
{
  "event": "execution.failed",
  "timestamp": "2024-01-15T10:30:15Z",
  "data": {
    "id": "exec-abc123...",
    "ensemble": "user-onboarding",
    "status": "failed",
    "error": {
      "message": "Database connection failed"
    },
    "duration": 567
  }
}
```

#### execution.timeout

Triggered when execution times out.

```json theme={null}
{
  "event": "execution.timeout",
  "timestamp": "2024-01-15T10:35:00Z",
  "data": {
    "id": "exec-abc123...",
    "duration": 30000,
    "timeout": 30000
  }
}
```

#### agent.completed

Triggered when individual agent completes.

```json theme={null}
{
  "event": "agent.completed",
  "timestamp": "2024-01-15T10:30:10Z",
  "data": {
    "executionId": "exec-abc123...",
    "agent": "create-account",
    "output": {
      "userId": "user_xyz789"
    },
    "duration": 234
  }
}
```

#### state.updated

Triggered when state changes.

```json theme={null}
{
  "event": "state.updated",
  "timestamp": "2024-01-15T10:30:12Z",
  "data": {
    "executionId": "exec-abc123...",
    "state": {
      "step": 3,
      "processed": 5
    }
  }
}
```

### Signature Verification

All webhook notifications include a signature for verification.

#### Headers

```http theme={null}
X-Conductor-Signature: sha256=abc123def456...
X-Conductor-Timestamp: 1705315200
X-Conductor-Event: execution.completed
X-Conductor-Delivery-Attempt: 1
```

#### Verify Signature (Node.js)

```javascript theme={null}
const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const signature = req.headers['x-conductor-signature'];
  const timestamp = req.headers['x-conductor-timestamp'];
  const body = JSON.stringify(req.body);

  // Prevent replay attacks (5 min window)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
    throw new Error('Webhook timestamp too old');
  }

  // Compute expected signature
  const payload = `${timestamp}.${body}`;
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // Compare signatures (timing-safe)
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid webhook signature');
  }

  return true;
}

// Express middleware
app.post('/webhooks/conductor', (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);

    // Process notification
    const { event, data } = req.body;
    console.log(`Received ${event}:`, data);

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook verification failed:', error);
    res.status(401).json({ error: error.message });
  }
});
```

#### Verify Signature (Python)

```python theme={null}
import hmac
import hashlib
import time

def verify_webhook_signature(request, secret):
    signature = request.headers.get('X-Conductor-Signature')
    timestamp = request.headers.get('X-Conductor-Timestamp')
    body = request.get_data(as_text=True)

    # Prevent replay attacks
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        raise ValueError('Webhook timestamp too old')

    # Compute expected signature
    payload = f"{timestamp}.{body}"
    expected_signature = 'sha256=' + hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Compare signatures (timing-safe)
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Invalid webhook signature')

    return True
```

### Handling Notifications

#### Basic Handler

```javascript theme={null}
app.post('/webhooks/conductor', async (req, res) => {
  // Verify signature
  verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);

  const { event, data } = req.body;

  // Handle event
  switch (event) {
    case 'execution.completed':
      await handleExecutionCompleted(data);
      break;

    case 'execution.failed':
      await handleExecutionFailed(data);
      await sendAlert(data);
      break;

    default:
      console.log(`Unhandled event: ${event}`);
  }

  // Respond quickly
  res.status(200).json({ received: true });
});
```

#### Async Processing

```javascript theme={null}
app.post('/webhooks/conductor', async (req, res) => {
  // Verify and respond quickly
  verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);
  res.status(200).json({ received: true });

  // Process asynchronously
  const { event, data } = req.body;

  // Queue for processing
  await queue.add('webhook', { event, data });
});
```

#### Error Handling

```javascript theme={null}
app.post('/webhooks/conductor', async (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.WEBHOOK_SECRET);

    const { event, data } = req.body;
    await processNotification(event, data);

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);

    // Return 5xx to trigger retry
    res.status(500).json({ error: 'Processing failed' });
  }
});
```

### Retry Logic

Conductor retries failed webhook notifications with exponential backoff:

1. **1 second** - First retry
2. **5 seconds** - Second retry
3. **30 seconds** - Third retry
4. **2 minutes** - Fourth retry
5. **5 minutes** - Final retry

**Total attempts**: Up to 3 retries (configurable)

**Status codes that trigger retry**:

* `5xx` - Server errors
* `408` - Request Timeout
* Connection errors / timeouts

**Status codes that don't retry**:

* `2xx`, `3xx` - Success
* `4xx` - Client errors (except 408)

## Email Notifications

Send email alerts for ensemble events.

### Configure Email Notifications

```yaml theme={null}
name: critical-workflow

notifications:
  - type: email
    to:
      - oncall@example.com
      - devops@example.com
    from: conductor@example.com
    subject: "[${event}] ${ensemble.name}"
    events:
      - execution.failed
      - execution.timeout

flow:
  - agent: critical-task

agents:
  - name: critical-task
    operation: think
    config:
      # ... your agent config

outputs:
  result: ${critical-task.output}
```

### Subject Template Variables

* `${event}` - Event type (e.g., "execution.failed")
* `${ensemble.name}` - Ensemble name
* `${timestamp}` - Event timestamp

### Email Content

Emails include both plain text and HTML versions:

* **Plain Text**: JSON-formatted event data
* **HTML**: Styled template with color-coded event types
  * Green: execution.completed
  * Red: execution.failed, execution.timeout
  * Blue: Other events

## Testing Webhooks

### Local Testing with ngrok

```bash theme={null}
# Start ngrok
ngrok http 3000

# Test inbound webhook
curl -X POST https://abc123.ngrok.io/webhooks/test \
  -H "Authorization: Bearer test-token" \
  -H "Content-Type: application/json" \
  -d '{"test": true}'
```

### Test Notification Delivery

```bash theme={null}
# Send test notification webhook
curl -X POST https://your-api.example.com/webhooks/conductor \
  -H "Content-Type: application/json" \
  -H "X-Conductor-Signature: sha256=..." \
  -H "X-Conductor-Timestamp: $(date +%s)" \
  -H "X-Conductor-Event: execution.completed" \
  -d '{
    "event": "execution.completed",
    "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
    "data": {
      "id": "exec-test123",
      "ensemble": "test-ensemble",
      "status": "completed"
    }
  }'
```

## Best Practices

### 1. Verify Signatures

```yaml theme={null}
# Always configure secrets for notifications
notifications:
  - type: webhook
    url: https://api.example.com/webhooks
    secret: ${env.WEBHOOK_SECRET}  # Required for security
```

```javascript theme={null}
// Always verify before processing
verifyWebhookSignature(req, secret);
```

### 2. Respond Quickly

```javascript theme={null}
// Respond within 5 seconds to avoid timeouts
res.status(200).json({ received: true });

// Process async
queueProcessor.add(event, data);
```

### 3. Handle Idempotency

```javascript theme={null}
// Store processed notification IDs
const processed = await db.notifications.findOne({
  id: data.id,
  event: event
});
if (processed) {
  return res.status(200).json({ received: true });
}

// Process and store
await processNotification(event, data);
await db.notifications.insert({
  id: data.id,
  event,
  processedAt: new Date()
});
```

### 4. Log All Notifications

```javascript theme={null}
await db.webhooks.insert({
  id: data.id,
  event,
  data,
  receivedAt: new Date(),
  attempt: req.headers['x-conductor-delivery-attempt']
});
```

### 5. Monitor Failures

```javascript theme={null}
const attempt = parseInt(req.headers['x-conductor-delivery-attempt']);
if (attempt > 1) {
  console.warn(`Retry attempt ${attempt} for ${event}`);
}

// Alert on persistent failures
if (recentFailures > 3) {
  await alerts.notify('Webhook failures detected', {
    event,
    failures: recentFailures
  });
}
```

### 6. Use Default-Deny Security

```yaml theme={null}
# GOOD: Explicit authentication
trigger:
  - type: webhook
    path: /webhooks/secure
    auth:
      type: bearer
      secret: ${env.SECRET}

# GOOD: Explicitly public
trigger:
  - type: webhook
    path: /webhooks/public
    public: true

# BAD: No auth, not marked public (will be rejected)
trigger:
  - type: webhook
    path: /webhooks/unsafe  # Error: requires auth or public: true
```

## Next Steps

<CardGroup cols={2}>
  <Card title="MCP Integration" icon="plug" href="/conductor/building/tools-mcp-integration">
    Expose ensembles as MCP tools
  </Card>

  <Card title="Email Operation" icon="envelope" href="/conductor/operations/email">
    Trigger ensembles via email
  </Card>

  <Card title="Authentication" icon="key" href="/api/http/authentication">
    Security best practices
  </Card>

  <Card title="Event-Driven" icon="bolt" href="/conductor/playbooks/event-driven-workflow">
    Event patterns
  </Card>
</CardGroup>
