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 trigger
  • node-id: Which node in the workflow receives the webhook
  • secure-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:

  1. Is subscription active in external service?
  2. Is webhook URL correct?
  3. Is firewall blocking requests?
  4. Are tokens valid?
  5. 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)

API References #

Next Steps #

  1. Learn about Authentication for webhook application credentials
  2. Understand Connectors for processing webhook data
  3. Read about Error Handling for webhook failures
  4. Explore Data Flow to understand webhook message routing
© 2025 Orchesty Solutions. All rights reserved.