Webhooks
What are Webhooks? #
Webhooks are HTTP callbacks that allow external services to notify your Orchesty workflows when events occur. Instead of continuously polling an API to check for changes, webhooks enable real-time, event-driven integrations where the external service pushes data to Orchesty when something happens.
Key concepts:
- Push vs Pull: Services send data to you (push) instead of you fetching it (pull)
- Event-driven: Triggered by specific events (order created, file uploaded, etc.)
- Webhook subscriptions: Registering your endpoint with external services
- Webhook tokens: Secure, unique URLs for each webhook
- Starting points: Webhooks enter topologies at specific nodes
Why Use Webhooks? #
Real-Time Processing #
- Immediate reactions: Process events as they happen
- No polling overhead: No wasted API calls checking for changes
- Lower latency: Sub-second response to events
- Better user experience: Instant notifications and updates
Efficiency #
- Reduced API calls: Only receive data when events occur
- Lower costs: Fewer API requests = lower bills
- Less load: No continuous polling drains resources
- Scalability: Handle high event volumes efficiently
Common Use Cases #
- Order notifications: New orders from e-commerce platforms
- Form submissions: Contact forms, surveys, registrations
- File uploads: Document processing, image uploads
- Status changes: Payment completed, shipment delivered
- Repository events: Code pushes, pull requests, issues
- Calendar events: Meeting scheduled, reminder triggered
How Webhooks Work #
The Webhook Flow #
sequenceDiagram
participant Admin as Orchesty Admin
participant App as Your Application
participant ExtAPI as External API
participant Webhook as Webhook Endpoint
participant Topo as Topology
Admin->>App: Subscribe webhook
App->>App: Generate secure token
App->>ExtAPI: Register webhook URL with token
ExtAPI-->>App: Confirm subscription
App->>Admin: Show subscribed
Note over ExtAPI: Event occurs
ExtAPI->>Webhook: POST event data to webhook URL
Webhook->>Webhook: Validate token
Webhook->>Topo: Start topology with event data
Topo-->>Webhook: Process complete
Webhook-->>ExtAPI: 200 OK
Webhook URL Structure #
https://orchesty.example.com/webhook/topologies/{topology-id}/nodes/{node-id}/token/{secure-token}
Components:
topology-id: Which workflow to triggernode-id: Which node in the workflow receives the webhooksecure-token: Unique token for security and routing
Implementing Webhook Applications #
Step 1: Extend IWebhookApplication #
import ABasicApplication from '@orchesty/nodejs-sdk/lib/Application/Base/ABasicApplication';
import { IWebhookApplication } from '@orchesty/nodejs-sdk/lib/Application/Base/IWebhookApplication';
import { ApplicationInstall } from '@orchesty/nodejs-sdk/lib/Application/Database/ApplicationInstall';
import RequestDto from '@orchesty/nodejs-sdk/lib/Transport/Curl/RequestDto';
import ResponseDto from '@orchesty/nodejs-sdk/lib/Transport/Curl/ResponseDto';
import { HttpMethods } from '@orchesty/nodejs-sdk/lib/Transport/HttpMethods';
import ProcessDto from '@orchesty/nodejs-sdk/lib/Utils/ProcessDto';
import WebhookSubscription from '@orchesty/nodejs-sdk/lib/Application/Model/Webhook/WebhookSubscription';
export default class ShopifyApplication extends ABasicApplication implements IWebhookApplication {
public getName(): string {
return 'shopify';
}
// ... other application methods ...
// Define available webhook subscriptions
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
new WebhookSubscription(
'orders/create', // subscription name
'webhook', // node name in topology
'orders/create', // topic/event type
[] // additional parameters
),
new WebhookSubscription(
'customers/create',
'webhook',
'customers/create',
[]
),
new WebhookSubscription(
'products/update',
'webhook',
'products/update',
[]
)
];
}
// Create request to subscribe to webhook
public getWebhookSubscribeRequestDto(
applicationInstall: ApplicationInstall,
subscription: WebhookSubscription,
url: string
): RequestDto {
const requestDto = this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.POST,
'/admin/api/2024-01/webhooks.json'
);
requestDto.setJsonBody({
webhook: {
topic: subscription.getTopic(),
address: url,
format: 'json'
}
});
return requestDto;
}
// Process subscription response from external API
public processWebhookSubscribeResponse(
dto: ResponseDto,
applicationInstall: ApplicationInstall
): string {
const response = JSON.parse(dto.getBody());
// Return webhook ID from external service
return response.webhook.id.toString();
}
// Create request to unsubscribe webhook
public getWebhookUnsubscribeRequestDto(
applicationInstall: ApplicationInstall,
webhook: any
): RequestDto {
const webhookId = webhook.getWebhookId();
return this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.DELETE,
`/admin/api/2024-01/webhooks/${webhookId}.json`
);
}
// Process unsubscription response
public processWebhookUnsubscribeResponse(dto: ResponseDto): boolean {
// Return true if unsubscription was successful
return dto.getResponseCode() === 200;
}
}
Step 2: Define Webhook Subscriptions #
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
new WebhookSubscription(
'order-created', // Name shown in UI
'process-order', // Node name in topology
'orders/create', // Event topic (API-specific)
[] // Additional parameters
),
new WebhookSubscription(
'customer-created',
'process-customer',
'customers/create',
[]
)
];
}
Step 3: Register Application #
import DIContainer from '@orchesty/nodejs-sdk/lib/DIContainer/Container';
import ShopifyApplication from './applications/ShopifyApplication';
const container = new DIContainer();
container.setApplication(new ShopifyApplication());
Webhook Subscription Management #
Subscribing Webhooks via API #
POST /webhook/applications/shopify/users/user@example.com/subscribe
Content-Type: application/json
{
"name": "order-created",
"topology": "order-processing-topology"
}
Unsubscribing Webhooks #
POST /webhook/applications/shopify/users/user@example.com/unsubscribe
Content-Type: application/json
{
"name": "order-created",
"topology": "order-processing-topology"
}
Listing Webhooks #
GET /webhook/applications/shopify/users/user@example.com
Response:
[
{
"name": "order-created",
"topology": "order-processing-topology",
"node": "process-order",
"webhookId": "12345",
"token": "abc123def456"
}
]
Processing Webhook Data #
Webhook Receiver Node #
In your topology, the webhook data arrives as a regular message:
import AConnector from '@orchesty/nodejs-sdk/lib/Connector/AConnector';
import ProcessDto from '@orchesty/nodejs-sdk/lib/Utils/ProcessDto';
export default class ProcessOrderWebhook extends AConnector {
public getName(): string {
return 'process-order-webhook';
}
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
// Webhook data arrives as JSON
const webhookData = dto.getJsonData();
console.log('Received webhook:', webhookData);
// Extract order data
const order = {
id: webhookData.id,
customer: webhookData.customer,
total: webhookData.total_price,
items: webhookData.line_items
};
// Process the order
const result = await this.processOrder(order);
dto.setJsonData(result);
return dto;
}
private async processOrder(order: any): Promise<any> {
// Your business logic here
return { status: 'processed', orderId: order.id };
}
}
Common Webhook Patterns #
Pattern 1: GitHub Webhooks #
import ABasicApplication from '@orchesty/nodejs-sdk/lib/Application/Base/ABasicApplication';
import { IWebhookApplication } from '@orchesty/nodejs-sdk/lib/Application/Base/IWebhookApplication';
import WebhookSubscription from '@orchesty/nodejs-sdk/lib/Application/Model/Webhook/WebhookSubscription';
export default class GitHubApplication extends ABasicApplication implements IWebhookApplication {
public getName(): string {
return 'github';
}
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
new WebhookSubscription('push', 'webhook', 'push', []),
new WebhookSubscription('pull_request', 'webhook', 'pull_request', []),
new WebhookSubscription('issues', 'webhook', 'issues', []),
new WebhookSubscription('release', 'webhook', 'release', [])
];
}
public getWebhookSubscribeRequestDto(
applicationInstall: ApplicationInstall,
subscription: WebhookSubscription,
url: string
): RequestDto {
const settings = applicationInstall.getSettings();
const repo = settings['config_form']['repository']; // e.g., "owner/repo"
const requestDto = this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.POST,
`/repos/${repo}/hooks`
);
requestDto.setJsonBody({
name: 'web',
active: true,
events: [subscription.getTopic()],
config: {
url: url,
content_type: 'json',
insecure_ssl: '0'
}
});
return requestDto;
}
public processWebhookSubscribeResponse(
dto: ResponseDto,
applicationInstall: ApplicationInstall
): string {
const response = JSON.parse(dto.getBody());
return response.id.toString();
}
public getWebhookUnsubscribeRequestDto(
applicationInstall: ApplicationInstall,
webhook: any
): RequestDto {
const settings = applicationInstall.getSettings();
const repo = settings['config_form']['repository'];
const hookId = webhook.getWebhookId();
return this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.DELETE,
`/repos/${repo}/hooks/${hookId}`
);
}
public processWebhookUnsubscribeResponse(dto: ResponseDto): boolean {
return dto.getResponseCode() === 204;
}
}
Pattern 2: Stripe Webhooks #
export default class StripeApplication extends ABasicApplication implements IWebhookApplication {
public getName(): string {
return 'stripe';
}
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
new WebhookSubscription('payment_succeeded', 'webhook', 'payment_intent.succeeded', []),
new WebhookSubscription('payment_failed', 'webhook', 'payment_intent.payment_failed', []),
new WebhookSubscription('customer_created', 'webhook', 'customer.created', []),
new WebhookSubscription('subscription_created', 'webhook', 'customer.subscription.created', [])
];
}
public getWebhookSubscribeRequestDto(
applicationInstall: ApplicationInstall,
subscription: WebhookSubscription,
url: string
): RequestDto {
const requestDto = this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.POST,
'/v1/webhook_endpoints'
);
requestDto.setJsonBody({
url: url,
enabled_events: [subscription.getTopic()],
api_version: '2024-11-20'
});
return requestDto;
}
public processWebhookSubscribeResponse(
dto: ResponseDto,
applicationInstall: ApplicationInstall
): string {
const response = JSON.parse(dto.getBody());
return response.id;
}
public getWebhookUnsubscribeRequestDto(
applicationInstall: ApplicationInstall,
webhook: any
): RequestDto {
return this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.DELETE,
`/v1/webhook_endpoints/${webhook.getWebhookId()}`
);
}
public processWebhookUnsubscribeResponse(dto: ResponseDto): boolean {
const response = JSON.parse(dto.getBody());
return response.deleted === true;
}
}
Pattern 3: Multiple Webhooks per Event #
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
// Same event, different nodes in topology
new WebhookSubscription(
'order-to-erp',
'erp-sync-node',
'orders/create',
[]
),
new WebhookSubscription(
'order-to-warehouse',
'warehouse-node',
'orders/create',
[]
),
new WebhookSubscription(
'order-notification',
'notification-node',
'orders/create',
[]
)
];
}
Pattern 4: Webhook with Custom Parameters #
public getWebhookSubscriptions(): WebhookSubscription[] {
return [
new WebhookSubscription(
'high-value-orders',
'webhook',
'orders/create',
['high_value'] // Custom parameter
)
];
}
public getWebhookSubscribeRequestDto(
applicationInstall: ApplicationInstall,
subscription: WebhookSubscription,
url: string
): RequestDto {
const requestDto = this.getRequestDto(
new ProcessDto(),
applicationInstall,
HttpMethods.POST,
'/webhooks'
);
const body: any = {
url: url,
event: subscription.getTopic()
};
// Add custom filter based on parameters
if (subscription.getParameters().includes('high_value')) {
body.filter = {
min_value: 1000
};
}
requestDto.setJsonBody(body);
return requestDto;
}
Webhook Security #
Token Validation #
Orchesty automatically validates webhook tokens. The external service calls:
POST https://orchesty.example.com/webhook/topologies/topo-123/nodes/node-456/token/abc123def456
If the token doesn't match, the request is rejected with 403 Forbidden.
Additional Security #
Some services send signature headers for extra validation:
// In your webhook processor connector
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
// Validate signature (example for Shopify)
const signature = dto.getHeader('x-shopify-hmac-sha256');
const rawBody = dto.getData();
if (!this.verifySignature(rawBody, signature)) {
dto.setStopProcess(
ResultCode.STOP_AND_FAILED,
'Invalid webhook signature'
);
return dto;
}
// Process webhook...
return dto;
}
private verifySignature(body: string, signature: string): boolean {
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', this.webhookSecret);
const calculatedSignature = hmac.update(body).digest('base64');
return calculatedSignature === signature;
}
Webhook Testing #
Testing Subscription #
# List webhooks for application
curl http://localhost:8080/webhook/applications/shopify/users/test@example.com
# Subscribe webhook
curl -X POST http://localhost:8080/webhook/applications/shopify/users/test@example.com/subscribe \
-H "Content-Type: application/json" \
-d '{"name":"order-created","topology":"order-processing"}'
Simulating Webhook Events #
# Send test webhook
curl -X POST https://orchesty.example.com/webhook/topologies/topo-123/nodes/node-456/token/abc123def456 \
-H "Content-Type: application/json" \
-d '{
"event": "order.created",
"order_id": "12345",
"total": 150.00
}'
Using Webhook Testing Tools #
- RequestBin: Capture and inspect webhook requests
- Ngrok: Tunnel localhost for testing
- Postman: Send mock webhook payloads
- Webhook.site: Online webhook testing
Monitoring Webhooks #
Webhook Logs #
import logger from '@orchesty/nodejs-sdk/lib/Logger/Logger';
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
const webhookData = dto.getJsonData();
logger.info(
`Received webhook: ${webhookData.event_type}`,
dto
);
// Process...
logger.info(
`Webhook processed successfully: ${webhookData.id}`,
dto
);
return dto;
}
Webhook Failures #
In Orchesty Admin, you can see:
- Failed webhook deliveries
- Retry attempts
- Error messages
- Webhook payload
Best Practices #
1. Idempotent Processing #
Webhooks may be delivered multiple times:
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
const event = dto.getJsonData();
// Check if already processed
const existing = await this.checkIfProcessed(event.id);
if (existing) {
logger.info(`Event ${event.id} already processed, skipping`, dto);
dto.setJsonData({ status: 'duplicate', processed: false });
return dto;
}
// Process and mark as processed
await this.processEvent(event);
await this.markAsProcessed(event.id);
dto.setJsonData({ status: 'success', processed: true });
return dto;
}
2. Quick Response #
Return 200 OK quickly, process asynchronously:
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
const webhook = dto.getJsonData();
// Validate quickly
if (!webhook.id) {
dto.setStopProcess(ResultCode.STOP_AND_FAILED, 'Missing ID');
return dto;
}
// Store for processing
dto.setJsonData(webhook);
// Follower nodes will do heavy processing
return dto;
}
3. Handle Missing Webhooks #
Not all events may arrive via webhook:
// Have a backup polling mechanism for critical data
// Check for missed webhooks periodically
4. Version Webhook Handlers #
public async processAction(dto: ProcessDto): Promise<ProcessDto> {
const webhook = dto.getJsonData();
// Handle different webhook versions
const version = dto.getHeader('x-webhook-version') || '1.0';
if (version === '2.0') {
return this.processV2(webhook, dto);
} else {
return this.processV1(webhook, dto);
}
}
5. Log Webhook Payloads #
logger.debug(
`Raw webhook payload: ${JSON.stringify(dto.getJsonData())}`,
dto
);
Troubleshooting #
Webhooks Not Received #
Check:
- Is subscription active in external service?
- Is webhook URL correct?
- Is firewall blocking requests?
- Are tokens valid?
- Is the service sending webhooks?
Duplicate Webhooks #
Some services retry automatically. Implement idempotency (see Best Practices #1).
Webhook Signature Validation Failing #
- Check secret key is correct
- Verify you're using correct hashing algorithm
- Ensure raw body is used (not parsed JSON)
Related Concepts #
- Authentication - Webhook applications need authentication
- Connector - Processing webhook data
- Data Flow - How webhook data flows through topologies
- Error Handling - Handling webhook processing errors
- Routing - Webhook endpoint routing
API References #
- IWebhookApplication - Webhook interface
- WebhookSubscription - Subscription model
- WebhookManager - Webhook management
- WebhookRouter - Webhook routing
- ABasicApplication - Application base class
Next Steps #
- Learn about Authentication for webhook application credentials
- Understand Connectors for processing webhook data
- Read about Error Handling for webhook failures
- Explore Data Flow to understand webhook message routing