How to Manage Webhook Triggers and Event Subscriptions for Real-Time Responses in Composio

Use composio.triggers.verifyWebhook() to validate HMAC-SHA256 signatures on incoming HTTP webhooks and composio.triggers.subscribe() to establish Pusher-based connections for streaming real-time trigger events with optional filtering.

Composio delivers real-time integration events through two primary channels: HTTP webhooks for push-based notifications and a Pusher-based subscription API for persistent event streaming. This guide demonstrates how to implement secure webhook verification and manage live event subscriptions using the Composio TypeScript SDK, referencing the actual implementation in the ComposioHQ/composio repository.

Verifying Inbound Webhook Signatures

All webhook requests from Composio include cryptographic signatures that must be validated before processing to ensure payload integrity and authenticity. The verification logic resides in ts/packages/core/src/models/Triggers.ts within the verifyWebhook method (lines 555-704).

The Verification Workflow

The Triggers.verifyWebhook function executes a four-step validation process:

  1. Parameter validation against VerifyWebhookParamsSchema
  2. Timestamp tolerance checking (defaulting to 5 minutes, configurable via the tolerance parameter)
  3. HMAC-SHA256 signature verification using the hmacSha256Base64 helper from ts/packages/core/src/utils/crypto.ts
  4. Payload parsing across version schemas

The signature format follows v1,base64Signature, computed by signing the string <webhook-id>.<timestamp>.<payload> with your webhook secret. The implementation uses timingSafeEqual to prevent timing attacks during the comparison operation.

Handling Multi-Version Payloads

Composio supports three webhook payload versions (V1-V3). The system attempts sequential parsing (V3 → V2 → V1) using tryParseVersionedPayload. If all schemas fail, the method throws ComposioWebhookPayloadError. Upon successful parsing, version-specific normalizers—normalizeV1Payload, normalizeV2Payload, and normalizeV3Payload (lines 558-736)—convert the data into a stable IncomingTriggerPayload structure regardless of the original version.

import { Composio, ComposioWebhookSignatureVerificationError } from '@composio/core';

// Inside your Express/Bun route handler
const result = await composio.triggers.verifyWebhook({
  payload: rawBody.toString(),
  signature: req.headers['webhook-signature'] as string,
  id: req.headers['webhook-id'] as string,
  timestamp: req.headers['webhook-timestamp'] as string,
  secret: process.env.COMPOSIO_WEBHOOK_SECRET!,
});

// result.payload is now a normalized IncomingTriggerPayload
console.log(result.payload.triggerSlug, result.payload.toolkitSlug);

Subscribing to Live Trigger Events

For real-time event streaming without managing HTTP endpoints, use the Pusher-based subscription system implemented in Triggers.subscribe (lines 434-465).

Establishing Pusher Connections

The subscription method creates a PusherService instance (defined in ts/packages/core/src/services/pusher/Pusher.ts) connecting to Composio's Pusher cluster. It validates optional filters through TriggerSubscribeParamSchema, then listens for raw messages that undergo the same version detection logic as webhooks via parsePusherPayload. Errors in your callback are logged but do not terminate the subscription loop.

Filtering Events by Toolkit and Type

Apply granular filters through the TriggerSubscribeParams interface to reduce processing overhead:

  • toolkits: Array of toolkit identifiers (e.g., ['github', 'slack'])
  • triggerSlug: Specific event types (e.g., ['GITHUB_PUSH_EVENT'])
  • connectedAccountId: Events scoped to a particular integration account
  • userId: User-specific event filtering
  • triggerData: Exact matching on metadata objects

The filtering logic executes in Triggers.shouldSendTriggerAfterFilters (lines 471-516).

await composio.triggers.subscribe(
  (payload) => {
    // Executed immediately when trigger fires
    console.log(`${payload.triggerSlug} from ${payload.toolkitSlug}`);
    // Dispatch to internal queues or processing logic
  },
  {
    toolkits: ['github'],
    triggerSlug: ['GITHUB_PUSH_EVENT', 'GITHUB_PULL_REQUEST_EVENT'],
    connectedAccountId: 'acc_12345'
  }
);

Managing Connection Lifecycle

Terminate subscriptions cleanly to prevent resource leaks, particularly during application shutdown or reconnection scenarios.

// Close Pusher socket and cleanup listeners
await composio.triggers.unsubscribe();

This method is implemented in Triggers.unsubscribe (lines 600-607) and ensures proper disposal of the underlying Pusher connection.

Security Best Practices

When managing webhook triggers and event subscriptions, implement these safeguards derived from ts/docs/advanced/webhook-verification.md (lines 38-46) and the crypto utilities in ts/packages/core/src/utils/crypto.ts:

  • Never expose webhook secrets in client-side code or version control; store COMPOSIO_WEBHOOK_SECRET in server-only environment variables
  • Always verify signatures using verifyWebhook before processing payload data
  • Enforce timestamp tolerance (default 5 minutes) to mitigate replay attacks
  • Use HTTPS endpoints for webhook receivers to prevent man-in-the-middle interception
  • Implement proper error handling for ComposioWebhookSignatureVerificationError (HTTP 401) and ComposioWebhookPayloadError (HTTP 400)

Complete Implementation Example

Reference the full working implementation in ts/examples/triggers/src/webhook-server.ts for a minimal Bun/Express server:

import express from 'express';
import { Composio, ComposioWebhookSignatureVerificationError } from '@composio/core';

const app = express();
const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });

app.post('/webhook', 
  express.raw({ type: 'application/json' }), 
  async (req, res) => {
    try {
      const result = await composio.triggers.verifyWebhook({
        payload: req.body.toString(),
        signature: req.headers['webhook-signature'] as string,
        id: req.headers['webhook-id'] as string,
        timestamp: req.headers['webhook-timestamp'] as string,
        secret: process.env.COMPOSIO_WEBHOOK_SECRET!,
      });

      console.log('✅ Verified', result.version, result.payload.triggerSlug);
      res.status(200).send('OK');
    } catch (e) {
      if (e instanceof ComposioWebhookSignatureVerificationError) {
        res.status(401).json({ error: 'Unauthorized' });
      } else {
        res.status(400).json({ error: 'Bad Request' });
      }
    }
  }
);

app.listen(3000);

Summary

Frequently Asked Questions

How do I verify webhook signatures in Composio?

Use the composio.triggers.verifyWebhook() method from ts/packages/core/src/models/Triggers.ts (lines 555-704). Pass the raw request body, signature header, webhook ID, timestamp, and your secret. The method validates the HMAC-SHA256 signature using the hmacSha256Base64 utility and performs timing-safe comparisons via timingSafeEqual to prevent timing attacks.

What is the difference between webhooks and Pusher subscriptions?

Webhooks deliver events via HTTP POST requests to your endpoint with signed payloads requiring cryptographic verification, ideal for serverless architectures. Pusher subscriptions maintain a persistent WebSocket connection through composio.triggers.subscribe(), providing immediate event streaming better suited for long-running applications requiring sub-second latency without HTTP overhead.

How do I filter specific trigger events in real-time?

Pass a filter object to composio.triggers.subscribe() specifying toolkits, triggerSlug, connectedAccountId, userId, or triggerData criteria. The SDK evaluates these filters in Triggers.shouldSendTriggerAfterFilters (lines 471-516) before invoking your callback, ensuring you process only relevant events.

How does Composio handle different webhook payload versions?

The SDK automatically normalizes V1, V2, and V3 payloads to the standard IncomingTriggerPayload interface through dedicated normalizer methods in ts/packages/core/src/models/Triggers.ts. The verifyWebhook function attempts parsing each version sequentially, providing backward compatibility without requiring manual version detection in your application code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →