Trigger Webhook
Trigger an ensemble via webhook. The path determines which ensemble to execute.
Webhook path (maps to ensemble name or custom route)
Webhook payload (forwarded as input to ensemble)
Signature for webhook verification (if configured)
Response
Whether webhook was received
Execution ID (if synchronous)
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
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...
}