Skip to main content
POST
/
webhooks
/
:path
curl -X POST https://your-worker.workers.dev/webhooks/github \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=..." \
  -d '{
    "action": "opened",
    "pull_request": {
      "number": 123,
      "title": "Add new feature"
    }
  }'
{
  "received": true,
  "executionId": "exec_abc123def456",
  "status": "completed",
  "output": {
    "processed": true
  }
}

Trigger Webhook

Trigger an ensemble via webhook. The path determines which ensemble to execute.
path
string
required
Webhook path (maps to ensemble name or custom route)
*
any
Webhook payload (forwarded as input to ensemble)
X-Webhook-Signature
string
Signature for webhook verification (if configured)

Response

received
boolean
Whether webhook was received
executionId
string
Execution ID (if synchronous)
queued
boolean
Whether execution was queued (if asynchronous)
curl -X POST https://your-worker.workers.dev/webhooks/github \
  -H "Content-Type: application/json" \
  -H "X-Hub-Signature-256: sha256=..." \
  -d '{
    "action": "opened",
    "pull_request": {
      "number": 123,
      "title": "Add new feature"
    }
  }'
{
  "received": true,
  "executionId": "exec_abc123def456",
  "status": "completed",
  "output": {
    "processed": true
  }
}

Webhook Configuration

Configure Routes

Map webhook paths to ensembles:
# ensemble.yaml
name: process-github-webhook
description: Handle GitHub webhook events

flow:
  - member: verify-signature
    type: Function
    input:
      payload: ${input}
      signature: ${headers['X-Hub-Signature-256']}
      secret: ${env.GITHUB_WEBHOOK_SECRET}

  - member: handle-event
    condition: ${verify-signature.output.valid}
    type: Function
    input:
      action: ${input.action}
      pullRequest: ${input.pull_request}

Worker Configuration

// src/index.ts
import { Conductor } from '@ensemble-edge/conductor';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Handle webhook routes
    if (url.pathname.startsWith('/webhooks/')) {
      const path = url.pathname.replace('/webhooks/', '');

      // Map paths to ensembles
      const ensembleMap: Record<string, string> = {
        'github': 'process-github-webhook',
        'stripe': 'process-stripe-webhook',
        'custom-event': 'handle-custom-event'
      };

      const ensemble = ensembleMap[path];
      if (!ensemble) {
        return Response.json(
          { error: 'Webhook path not found' },
          { status: 404 }
        );
      }

      // Parse webhook payload
      const body = await request.json();
      const headers = Object.fromEntries(request.headers);

      // Execute ensemble
      const conductor = new Conductor({ env });
      const result = await conductor.executeEnsemble(ensemble, {
        ...body,
        headers
      });

      if (result.ok) {
        return Response.json({
          received: true,
          executionId: result.value.id
        });
      } else {
        return Response.json(
          { error: result.error.message },
          { status: 500 }
        );
      }
    }

    return Response.json({ error: 'Not found' }, { status: 404 });
  }
};

Signature Verification

GitHub

import crypto from 'crypto';

async function verifyGitHubSignature(
  payload: string,
  signature: string,
  secret: string
): Promise<boolean> {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(payload);
  const expectedSignature = 'sha256=' + hmac.digest('hex');
  return signature === expectedSignature;
}

Stripe

async function verifyStripeSignature(
  payload: string,
  signature: string,
  secret: string
): Promise<boolean> {
  const parts = signature.split(',');
  const timestamp = parts.find(p => p.startsWith('t='))?.split('=')[1];
  const sig = parts.find(p => p.startsWith('v1='))?.split('=')[1];

  const signedPayload = `${timestamp}.${payload}`;
  const expectedSig = await crypto.subtle.sign(
    'HMAC',
    await crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(secret),
      { name: 'HMAC', hash: 'SHA-256' },
      false,
      ['sign']
    ),
    new TextEncoder().encode(signedPayload)
  );

  const expectedHex = Array.from(new Uint8Array(expectedSig))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  return sig === expectedHex;
}

Custom HMAC

# Verify in ensemble
- member: verify-signature
  type: Function
  input:
    payload: ${JSON.stringify(input)}
    signature: ${headers['X-Webhook-Signature']}
    secret: ${env.WEBHOOK_SECRET}
    algorithm: "sha256"

Webhook Patterns

Async Processing (Queue)

For long-running webhooks, queue for async processing:
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.url.endsWith('/webhooks/async')) {
      const body = await request.json();

      // Queue for processing
      await env.WEBHOOK_QUEUE.send({
        ensemble: 'process-webhook',
        input: body,
        timestamp: Date.now()
      });

      return Response.json({
        received: true,
        queued: true
      }, { status: 202 });
    }
  },

  async queue(batch: MessageBatch, env: Env): Promise<void> {
    const conductor = new Conductor({ env });

    for (const message of batch.messages) {
      const { ensemble, input } = message.body;
      await conductor.executeEnsemble(ensemble, input);
      message.ack();
    }
  }
};

Retry Logic

# Webhook ensemble with retry
name: process-webhook
description: Process webhook with retry

flow:
  - member: process-event
    type: Function
    retry:
      maxAttempts: 3
      backoff: exponential
    input:
      event: ${input}

Webhook Response

Return data in webhook response:
output:
  received: true
  processed: true
  resourceId: ${create-resource.output.id}
  url: "https://example.com/resources/${create-resource.output.id}"

Testing Webhooks

Local Testing

# Start local server
npx wrangler dev

# Test webhook
curl -X POST http://localhost:8787/webhooks/test \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

Webhook.site

Use webhook.site to inspect webhook payloads:
# Temporarily forward to webhook.site
- member: debug-webhook
  type: API
  config:
    url: "https://webhook.site/your-unique-url"
    method: POST
  input:
    body: ${input}

Common Webhooks

GitHub

// Map GitHub events to ensembles
const githubWebhooks: Record<string, string> = {
  'pull_request.opened': 'pr-opened',
  'pull_request.closed': 'pr-closed',
  'push': 'code-pushed',
  'release.published': 'release-published'
};

const event = `${body.action ? `${body.action}.` : ''}${headers['X-GitHub-Event']}`;
const ensemble = githubWebhooks[event];

Stripe

// Map Stripe events to ensembles
const stripeWebhooks: Record<string, string> = {
  'payment_intent.succeeded': 'payment-succeeded',
  'payment_intent.payment_failed': 'payment-failed',
  'customer.subscription.created': 'subscription-created',
  'customer.subscription.deleted': 'subscription-cancelled'
};

const ensemble = stripeWebhooks[body.type];

Slack

// Handle Slack slash commands
if (body.type === 'url_verification') {
  return Response.json({ challenge: body.challenge });
}

if (body.type === 'event_callback') {
  const ensemble = `slack-${body.event.type}`;
  // Execute ensemble...
}