# Configuring webhook endpoints (/docs/setting-up-triggers/configuring-webhook-endpoints)

Some toolkits — currently **Slack**, with more arriving on the same model — deliver events at the OAuth-app level rather than per-user. For these toolkits, you need to create a `webhook_endpoint` once per OAuth app, store the provider's signing secret on it, and point the provider's app dashboard at the URL Composio returns.

This is the only piece of trigger setup that's not automatic. Once it's done, [creating triggers](/docs/setting-up-triggers/creating-triggers) and [subscribing to events](/docs/setting-up-triggers/subscribing-to-events) work the same as for any other toolkit.

> Most toolkits don't need this — Composio registers per-user webhooks with the provider automatically when you create a trigger. See the [delivery models](/docs/triggers#three-delivery-models) table for which model your toolkit uses. If yours is **user-level webhook** or **polling**, skip this page entirely.

# What a webhook endpoint is

A `webhook_endpoint` is a project-scoped resource keyed by `(toolkit_slug, project_id, client_id)`. It exposes one ingress URL containing a random `we_*` identifier:

```
https://backend.composio.dev/api/v3.1/webhook_ingress/{toolkit_slug}/{we_xxx}/trigger_event
```

Provider events posted to this URL are verified at ingress against the signing secret stored on the endpoint, then fanned out only to trigger instances on that endpoint's project. Unsigned or tampered requests are rejected before any trigger fires.

> **OAuth apps are project-scoped on V2.** A `webhook_endpoint` ties one OAuth app (`client_id`) to exactly one project. If you share a single OAuth app across multiple Composio projects today, either consolidate to one project or register separate OAuth apps per project before creating a V2 endpoint. The provider dashboard accepts only one callback URL per OAuth app, so a single OAuth app cannot feed multiple projects on V2. See the [V2 announcement](/docs/changelog/2026/04/27#heads-up-oauth-apps-are-project-scoped-on-v2) for the full reasoning.

# End-to-end setup (Slack)

The flow below uses Slack because it's the first toolkit on V2. Substitute `slack` with any other V2 toolkit slug as more come online — the API surface is identical.

### Discover required fields

Each V2 toolkit has its own list of secrets it needs (signing secret, optionally an app-level token, etc.). Fetch the schema before creating the endpoint so you know what to collect from the provider's dashboard:

```bash
curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/schema?toolkit_slug=slack" \
  -H "x-api-key: "
```

Sample response:

```json
{
  "toolkit_slug": "slack",
  "setup_fields": {
    "webhook_signing_secret": {
      "display_name": "Signing Secret",
      "description": "Webhook request signing secret from your Slack app dashboard",
      "is_required": true,
      "is_secret": true
    },
    "app_token": {
      "display_name": "App-Level Token",
      "description": "Slack xapp- token with authorizations:read scope for event authorization",
      "is_required": true,
      "is_secret": true
    }
  }
}
```

### Create the endpoint

```bash
curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints" \
  -H "x-api-key: " \
  -H "Content-Type: application/json" \
  -d '{
    "toolkit_slug": "slack",
    "client_id": ""
  }'
```

Sample response:

```json
{
  "id": "we_abc123",
  "toolkit_slug": "slack",
  "client_id": "",
  "webhook_url": "https://backend.composio.dev/api/v3.1/webhook_ingress/slack/we_abc123/trigger_event",
  "data": null,
  "created_at": "2026-04-24T10:00:00.000Z"
}
```

Hold on to two values from the response: `id` (used as `` below) and `webhook_url` (you'll paste this into the provider's dashboard in step 5).

> The call is **idempotent per `(toolkit_slug, client_id)` within a project** — calling it again with the same pair returns the existing endpoint without rotating the URL or wiping the secret.

### Store the signing secret

```bash
curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \
  -H "x-api-key: " \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "webhook_signing_secret": ""
    }
  }'
```

For Slack, the signing secret lives at **Slack app → Basic Information → App Credentials → Signing Secret**.

> **Store the signing secret before switching the provider's callback URL.** If the provider posts to a V2 URL without a secret available, every request fails with `400` and the provider may auto-disable the endpoint. Slack, for example, disables URLs after \~36 hours of consecutive failures.

### Add an app-level token (Slack-specific, optional)

Slack uses an app-level token to resolve which connected user is authorized for a given event. This unlocks events that aren't otherwise scoped to a single user — DMs, private channels, and reactions:

    * `SLACK_DIRECT_MESSAGE_RECEIVED` — always required.
    * `SLACK_CHANNEL_MESSAGE_RECEIVED` — required for **private channels** and **multi-person DMs**; public-channel messages are delivered without it.
    * `SLACK_MESSAGE_REACTION_ADDED` — always required (reactions don't carry channel-type metadata, so per-user authorization runs unconditionally).

Skip this step if you only need public-channel messages today. You can add the token later by re-running the `PATCH` below.

```bash
curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \
  -H "x-api-key: " \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "app_token": "xapp-..."
    }
  }'
```

Generate the token from **Slack app → Basic Information → App-Level Tokens** with scope `authorizations:read`.

> **Other V2 toolkits will have different secondary fields.** Always read `setup_fields` from the schema endpoint (step 1) — the field names, descriptions, and which are required are the source of truth.

### Point the provider's app dashboard at the V2 URL

For Slack, set **Event Subscriptions → Request URL** to the `webhook_url` returned in step 2. Composio responds to Slack's `url_verification` challenge automatically — you don't need to write any handshake code on your side.

The same applies to other providers Composio supports on V2: Composio handles the verification challenge (Asana `X-Hook-Secret`, Outlook validation tokens, and so on) so the URL verifies on first save.

### Confirm the endpoint is verified

```bash
curl "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \
  -H "x-api-key: "
```

A populated `verified_at` timestamp on the response means the provider has successfully completed the handshake against your endpoint. Once `verified_at` is set, you can [create V2 trigger instances](/docs/setting-up-triggers/creating-triggers) that route through this endpoint.

# Updating an endpoint

Use `PATCH` to update one field at a time (e.g. rotate just the signing secret):

```bash
curl -X PATCH "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \
  -H "x-api-key: " \
  -H "Content-Type: application/json" \
  -d '{ "data": { "webhook_signing_secret": "" } }'
```

Use `POST` to **replace** `data` wholesale — any field you don't include is cleared:

```bash
curl -X POST "https://backend.composio.dev/api/v3.1/webhook_endpoints/" \
  -H "x-api-key: " \
  -H "Content-Type: application/json" \
  -d '{ "data": { "webhook_signing_secret": "", "app_token": "" } }'
```

The `webhook_url` itself is immutable — it's tied to the endpoint's `we_*` id for the lifetime of the endpoint. Rotating the signing secret on the provider side is a `PATCH` on the existing endpoint, not a new endpoint.

# Listing endpoints

```bash
curl "https://backend.composio.dev/api/v3.1/webhook_endpoints" \
  -H "x-api-key: "
```

Returns every endpoint in the current project with `verified_at` populated for the ones that have completed the handshake. Use this when you're operating across many OAuth apps and want a quick health view.

# Migrating from V1

If you already have V1 trigger instances running, your existing setup is unchanged — V1 ingress URLs (`/api/v3/trigger_instances/{toolkit}/{project_id}/handle`) are not deprecated and every legacy slug (`SLACK_RECEIVE_*`, `SLACKBOT_*`, etc.) keeps routing through V1.

You only need to set up a V2 webhook endpoint in two cases:

1. **You want to use a V2 trigger slug.** For Slack, that's `SLACK_CHANNEL_MESSAGE_RECEIVED`, `SLACK_DIRECT_MESSAGE_RECEIVED`, or `SLACK_MESSAGE_REACTION_ADDED`. Without an endpoint, the upsert call returns `400`.
2. **You want to consolidate an OAuth app onto V2.** Pick the project that should own the OAuth app on V2, set up an endpoint there, and update the provider's callback URL.

Even outside these cases, moving an OAuth app to V2 is recommended for the security wins — ingress signature verification, project-scoped fan-out, and per-user authorization for app-level events. See the [V2 announcement](/docs/changelog/2026/04/27) for the full migration walkthrough and the V1 → V2 slug mapping for Slack.

# What to read next

- [Creating triggers](/docs/setting-up-triggers/creating-triggers): Create trigger instances once your endpoint is configured

- [Subscribing to events](/docs/setting-up-triggers/subscribing-to-events): Set up the URL Composio delivers events to

- [Verifying webhooks](/docs/webhook-verification): Verify the signature on every Composio-to-your-endpoint delivery

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Glossary](https://docs.composio.dev/llms.mdx/docs/glossary) | [Cookbooks](https://docs.composio.dev/llms.mdx/cookbooks) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Instructions for AI Code Generators

**Purpose:** Enforce only the **current** and **correct** patterns for integrating [Composio](https://composio.dev/) tools into AI agents.
**Scope:** All AI-generated code using Composio must follow these guardrails.

---

## 1. Correct Composio Integration

Composio supports two integration modes: **Native Tools** (with a provider package) and **MCP** (no provider package needed).

### Native Tools

```python
# ✅ CORRECT — Python (defaults to OpenAI)
from composio import Composio

composio = Composio()
session = composio.create(user_id="user_123")
tools = session.tools()
# Pass tools to your agent/LLM framework
```

```typescript
// ✅ CORRECT — TypeScript (defaults to OpenAI)
import { Composio } from "@composio/core";

const composio = new Composio();
const session = await composio.create("user_123");
const tools = await session.tools();
// Pass tools to your agent/LLM framework
```

For other providers, pass the provider explicitly. Provider packages follow the naming convention: `composio_<provider>` for Python, `@composio/<provider>` for TypeScript.

### MCP

Use `session.mcp.url` and `session.mcp.headers` with any MCP-compatible client (Claude Desktop, Cursor, OpenAI Agents, etc.). No provider package needed.

---

## 2. CRITICAL INSTRUCTIONS FOR AI MODELS

### 2.1 — ALWAYS DO THE FOLLOWING

1. **Use `composio.create(user_id)`** to create a session — this is the entry point for all Composio integrations.
2. **Use `session.tools()`** for native tool integration or **`session.mcp.url`** for MCP integration.
3. **Import** from the correct provider package (`composio_<provider>` for Python, `@composio/<provider>` for TypeScript).
4. **Pass `user_id` to `composio.create()`**, not to individual tool calls.

### 2.2 — NEVER DO THE FOLLOWING

1. **Do not** instruct users to manually create auth configs or connected accounts before running.
2. **Do not** import from `composio_openai` / `@composio/openai` when using OpenAI Agents SDK — use `composio_openai_agents` / `@composio/openai-agents` instead.

### 2.3 — DISCOURAGED (do not use unless user explicitly asks)

Do **not** generate `composio.tools.get()`, `composio.tools.execute()`, or `composio.provider.handle_tool_calls()` unless the user explicitly requests direct tool execution. These are a supported but **not recommended** low-level interface.


---

## Terminology Migration (old → current)

If you encounter these terms in error messages, old documentation, or user prompts, translate them to the current equivalents. **Do not use the old terms in generated code or explanations.**

| Old term (v1/v2) | Current term (v3) | In code |
|---|---|---|
| entity ID | user ID | `user_id` parameter |
| actions | tools | e.g., `GITHUB_CREATE_ISSUE` is a *tool* |
| apps / appType | toolkits | e.g., `github` is a *toolkit* |
| integration / integration ID | auth config / auth config ID | `auth_config_id` parameter |
| connection | connected account | `connected_accounts` namespace |
| ComposioToolSet / OpenAIToolSet | `Composio` class with a provider | `Composio(provider=...)` |
| toolset | provider | e.g., `OpenAIProvider` |

If a user says "entity ID", they mean `user_id`. If they say "integration", they mean "auth config". Always respond using the current terminology.

