Using Triggers

Subscribing to triggers

Markdown

Webhooks

Webhooks are the recommended way to receive trigger events in production. To start receiving events, create a webhook subscription with your endpoint URL and select which event types you want to receive. You can subscribe to one or both:

Event typeDescription
composio.trigger.messageFired when a trigger receives data from an external service
composio.connected_account.expiredFired when a connected account expires and needs re-authentication. See Subscribing to connection expiry events.

Set your webhook URL in the dashboard settings or via the Webhook Subscriptions API:

curl -X POST https://backend.composio.dev/api/v3/webhook_subscriptions \
  -H "X-API-KEY: <your-composio-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://example.com/webhook",
    "enabled_events": ["composio.trigger.message"]
  }'

The response includes a secret for verifying webhook signatures. This is only returned at creation time or when you rotate the secret. Store it securely.

Handling events

All events arrive at the same endpoint. Route on the type field to handle each event type:

Inspect the payload schema for a trigger before writing your handler. See Webhook payload (V3) for the full event structure.

from composio import WebhookEventType

@app.post("/webhook")
async def webhook_handler(request: Request):
    payload = await request.json()
    event_type = payload.get("type")

    if event_type == WebhookEventType.TRIGGER_MESSAGE:
        trigger_slug = payload["metadata"]["trigger_slug"]
        event_data = payload["data"]

        if trigger_slug == "GITHUB_COMMIT_EVENT":
            print(f"New commit by {event_data['author']}: {event_data['message']}")

    # Handle connected account expired events

    return {"status": "ok"}
export default async function webhookHandler(req: NextApiRequest, res: NextApiResponse) {
  const payload = req.body;

  if (payload.type === 'composio.trigger.message') {
    const triggerSlug = payload.metadata.trigger_slug;
    const eventData = payload.data;

    if (triggerSlug === 'GITHUB_COMMIT_EVENT') {
      console.log(`New commit by ${eventData.author}: ${eventData.message}`);
    }
  }

  // Handle connected account expired events

  res.status(200).json({ status: 'ok' });
}

Always verify webhook signatures in production to ensure payloads are authentic.

Inspecting trigger payload schemas

Each trigger type defines the schema of event data it sends. Use get_type()/getType() to inspect it before writing your handler:

from composio import Composio

composio = Composio()

trigger_type = composio.triggers.get_type("GITHUB_COMMIT_EVENT")
print(trigger_type.payload)
# Returns: {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}}
import { Composio } from '@composio/core';

const composio = new Composio();

const triggerType = await composio.triggers.getType("GITHUB_COMMIT_EVENT");
console.log(triggerType.payload);
// Returns: {"properties": {"author": {...}, "id": {...}, "message": {...}, "timestamp": {...}, "url": {...}}}

The payload schema tells you what fields will be in the data object of the webhook event.

Webhook payload (V3)

New organizations receive V3 payloads by default. V3 separates event metadata from the actual event data:

{
  "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"
}

See webhook payload versions for V2 and V1 formats.

Testing locally

SDK subscriptions

Subscribe to trigger events directly through the SDK without setting up a webhook endpoint. Uses WebSockets under the hood.

from composio import Composio

composio = Composio()

subscription = composio.triggers.subscribe()

@subscription.handle(trigger_id="your_trigger_id")
def handle_event(data):
    print(f"Event received: {data}")

subscription.wait_forever()
import { Composio } from '@composio/core';

const composio = new Composio();

await composio.triggers.subscribe(
    (data) => {
        console.log('Event received:', data);
    },
    { triggerId: 'your_trigger_id' }
);

Using ngrok

To test the full webhook flow locally, use ngrok to expose your local server:

ngrok http 8000

Then use the ngrok URL as your webhook endpoint:

curl -X POST https://backend.composio.dev/api/v3/webhook_subscriptions \
  -H "X-API-KEY: <your-composio-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://your-ngrok-url.ngrok-free.app/webhook",
    "enabled_events": ["composio.trigger.message"]
  }'

Events will now be forwarded to your local server at http://localhost:8000/webhook.

Identifying trigger events

Every webhook event includes a metadata object that tells you exactly where it came from:

FieldWhat it tells you
metadata.trigger_idWhich trigger instance fired this event
metadata.trigger_slugThe type of trigger (e.g., GITHUB_COMMIT_EVENT)
metadata.connected_account_idWhich connected account it belongs to
metadata.user_idWhich user it's for
metadata.auth_config_idWhich auth config was used

Use trigger_id to match events to a specific trigger instance, or trigger_slug to handle all events of a certain type. These fields can also be passed as filters when using SDK subscriptions.