Verifying webhooks
Composio signs every webhook request. Always verify signatures in production to ensure payloads are authentic.
Two signatures, two responsibilities. Composio verifies provider signatures on the ingress hop (provider → Composio); you verify Composio's signature on the delivery hop (Composio → your endpoint). This page covers the delivery hop. Ingress verification happens automatically — see Ingress signature verification below.
SDK verification
The SDK handles signature verification, payload parsing, and version detection (V1, V2, V3).
Your webhook secret is returned only once: when you create a webhook subscription or rotate the secret. If you didn't copy it at creation time, rotate it to get a new one. Store it securely as COMPOSIO_WEBHOOK_SECRET.
try:
result = composio.triggers.verify_webhook(
id=request.headers.get("webhook-id", ""),
payload=request.get_data(as_text=True),
signature=request.headers.get("webhook-signature", ""),
timestamp=request.headers.get("webhook-timestamp", ""),
secret=os.getenv("COMPOSIO_WEBHOOK_SECRET", ""),
)
# result.version, result.payload, result.raw_payload
except Exception:
return {"error": "Invalid signature"}, 401try {
const result = await composio.triggers.verifyWebhook({
id: req.headers['webhook-id'],
payload: req.body,
signature: req.headers['webhook-signature'],
timestamp: req.headers['webhook-timestamp'],
secret: process.env.COMPOSIO_WEBHOOK_SECRET!,
});
// result.version, result.payload, result.rawPayload
} catch (error) {
// Return 401
}An optional tolerance parameter (default: 300 seconds) controls how old a webhook can be before verification fails. Set to 0 to disable timestamp validation.
Manual verification
If you are not using the Composio SDK and want to verify signatures manually.
Your webhook secret is returned only once: when you create a webhook subscription or rotate the secret. If you didn't copy it at creation time, rotate it to get a new one. Store it securely as COMPOSIO_WEBHOOK_SECRET.
Every webhook request includes three headers: webhook-signature, webhook-id, and webhook-timestamp. Use these along with the raw request body to verify the signature:
import hmac
import hashlib
import base64
import json
import os
def verify_webhook(webhook_id: str, webhook_timestamp: str, body: str, signature: str) -> dict:
secret = os.getenv("COMPOSIO_WEBHOOK_SECRET", "")
signing_string = f"{webhook_id}.{webhook_timestamp}.{body}"
expected = base64.b64encode(
hmac.new(secret.encode(), signing_string.encode(), hashlib.sha256).digest()
).decode()
received = signature.split(",", 1)[1] if "," in signature else signature
if not hmac.compare_digest(expected, received):
raise ValueError("Invalid webhook signature")
payload = json.loads(body)
# V3 payload
return {
"trigger_slug": payload["metadata"]["trigger_slug"],
"data": payload["data"],
}function verifyWebhook(
webhookId: string,
webhookTimestamp: string,
body: string,
signature: string
) {
const secret = process.env.COMPOSIO_WEBHOOK_SECRET ?? '';
const signingString = `${webhookId}.${webhookTimestamp}.${body}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signingString)
.digest('base64');
const received = signature.split(',')[1] ?? signature;
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
throw new Error('Invalid webhook signature');
}
const payload = JSON.parse(body);
// V3 payload
return {
triggerSlug: payload.metadata.trigger_slug,
data: payload.data,
};
}Webhook payload versions
verifyWebhook() auto-detects the version. If you process payloads manually, here are the formats:
Metadata is separated from event data. New organizations receive V3 payloads by default.
{
"id": "msg_abc123",
"type": "composio.trigger.message",
"metadata": {
"log_id": "log_abc123",
"trigger_slug": "GITHUB_COMMIT_EVENT",
"trigger_id": "ti_xyz789",
"connected_account_id": "ca_def456",
"auth_config_id": "ac_xyz789",
"user_id": "user-id-123435"
},
"data": {
"commit_sha": "a1b2c3d",
"message": "fix: resolve null pointer",
"author": "jane"
},
"timestamp": "2026-01-15T10:30:00Z"
}Metadata fields are mixed into the data object alongside event data.
{
"type": "github_commit_event",
"data": {
"commit_sha": "a1b2c3d",
"message": "fix: resolve null pointer",
"author": "jane",
"connection_id": "ca_def456",
"connection_nano_id": "cn_abc123",
"trigger_nano_id": "tn_xyz789",
"trigger_id": "ti_xyz789",
"user_id": "user-id-123435"
},
"timestamp": "2026-01-15T10:30:00Z",
"log_id": "log_abc123"
}{
"trigger_name": "github_commit_event",
"trigger_id": "ti_xyz789",
"connection_id": "ca_def456",
"payload": {
"commit_sha": "a1b2c3d",
"message": "fix: resolve null pointer",
"author": "jane"
},
"log_id": "log_abc123"
}Ingress signature verification
Composio verifies the provider's signature on every inbound request against the signing secret stored on the webhook endpoint before any trigger fires. Unsigned or tampered requests are rejected at ingress with 400, so third parties cannot spoof events onto your triggers.
This hop is automatic — you don't write any code. Your responsibility is just to make sure the signing secret on the endpoint is correct:
- Composio-managed OAuth apps — Composio sources the signing secret from the connected user's credentials; nothing to do.
- Your own OAuth app on a trigger whose
requires_webhook_endpointflag is true —PATCHthe signing secret onto the endpoint as part of Configuring the webhook endpoint:
curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/<ENDPOINT_ID>" \
-H "x-api-key: <YOUR_COMPOSIO_API_KEY>" \
-H "Content-Type: application/json" \
-d '{ "data": { "webhook_signing_secret": "<SIGNING_SECRET>" } }'Composio uses the algorithm each provider expects — HMAC-SHA256 for Slack, Ed25519 or shared-token matching for others. For providers that sign a request timestamp (Slack does), replay protection additionally rejects requests outside the allowed skew window.
What to read next
Subscribing to events
Set up webhooks and SDK subscriptions to receive trigger events
Creating triggers
Configure the webhook endpoint when needed and create trigger instances
Triggers overview
How Composio delivers event data from connected apps
Connection expiry events
Detect when OAuth connections expire and prompt re-authentication