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:
- Parameter validation against
VerifyWebhookParamsSchema - Timestamp tolerance checking (defaulting to 5 minutes, configurable via the
toleranceparameter) - HMAC-SHA256 signature verification using the
hmacSha256Base64helper fromts/packages/core/src/utils/crypto.ts - 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 accountuserId: User-specific event filteringtriggerData: 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_SECRETin server-only environment variables - Always verify signatures using
verifyWebhookbefore 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) andComposioWebhookPayloadError(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
- Verify signatures using
composio.triggers.verifyWebhook()with your secret to authenticate HMAC-SHA256 signatures and normalize payloads toIncomingTriggerPayload - Handle versioning automatically through the SDK's sequential parsing of V1-V3 schemas in
ts/packages/core/src/models/Triggers.ts - Subscribe to real-time events via
composio.triggers.subscribe()using Pusher connections, applying filters fortoolkits,triggerSlug, orconnectedAccountIdto reduce noise - Cleanup resources by calling
composio.triggers.unsubscribe()during application shutdown to close sockets properly - Consult reference files:
ts/packages/core/src/models/Triggers.ts,ts/packages/core/src/utils/crypto.ts, andts/packages/core/src/types/triggers.types.tsfor implementation details
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →