> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corsair.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time events from any plugin in three steps.

Webhooks let external services push events to you the moment something happens — a GitHub star, a Slack message, a PR merged. You expose one endpoint. Corsair verifies the signature, identifies the plugin, and calls your handler.

**Three steps: ngrok → register → react.**

***

## Step 1 — Expose a public URL

GitHub and Slack can't reach `localhost`. Use ngrok to tunnel your local server:

```bash theme={null}
# Install ngrok — https://ngrok.com/download
ngrok http 3000
```

You'll get a URL like `https://abc123.ngrok-free.app`. Copy it.

<Info>
  **Get a stable URL (recommended)**
  Free ngrok accounts get a random URL on every restart — meaning you'd have to re-register your webhook every time. Claim a **free static domain** at [dashboard.ngrok.com/domains](https://dashboard.ngrok.com/domains) and it'll never change.
</Info>

***

## Step 2 — Create the webhook endpoint

Add one route to your server. All plugins share this single URL:

```ts app/api/webhook/route.ts theme={null}
import { processWebhook } from 'corsair';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { corsair } from '@/server/corsair';

export async function POST(request: NextRequest) {
    const headers: Record<string, string> = {};
    request.headers.forEach((value, key) => { headers[key] = value; });

    const body = request.headers.get('content-type')?.includes('application/json')
        ? await request.json()
        : await request.text();

    // Include tenantId if you're using multi-tenancy
    const tenantId = new URL(request.url).searchParams.get('tenantId') ?? undefined;

    const result = await processWebhook(corsair, headers, body, { tenantId });

    if (!result.response) {
        return NextResponse.json({ success: false }, { status: 404 });
    }

    return NextResponse.json(result.response);
}
```

`processWebhook` handles everything: it identifies which plugin sent the event, verifies the signature, updates your local database, and runs your hooks.

***

## Step 3 — Register and react

<Tabs>
  <Tab title="GitHub">
    **Register in GitHub:**

    1. Go to your repo → **Settings → Webhooks → Add webhook**
    2. Set **Payload URL**: `https://your-ngrok-url.ngrok-free.app/api/webhook`
    3. Set **Content type**: `application/json`
    4. Add a **Secret** — save it
    5. Choose events (or "Send me everything")
    6. Click **Add webhook**

    **Store the secret:**

    ```ts theme={null}
    await corsair.github.keys.set_webhook_signature(process.env.GITHUB_WEBHOOK_SECRET!);
    ```

    **React to events:**

    ```ts corsair.ts theme={null}
    github({
        webhookHooks: {
            starCreated: {
                after: async (ctx, result) => {
                    console.log(`⭐ ${result?.data?.sender?.login} starred ${result?.data?.repository?.full_name}`);
                },
            },
            pullRequestOpened: {
                after: async (ctx, result) => {
                    const pr = result?.data?.pull_request;
                    console.log(`PR opened: "${pr?.title}" by ${pr?.user?.login}`);
                },
            },
            push: {
                after: async (ctx, result) => {
                    const branch = result?.data?.ref.replace('refs/heads/', '');
                    console.log(`${result?.data?.commits?.length} commit(s) pushed to ${branch}`);
                },
            },
        },
    })
    ```
  </Tab>

  <Tab title="Slack">
    **Register in Slack:**

    1. Go to [api.slack.com/apps](https://api.slack.com/apps) → your app
    2. Navigate to **Event Subscriptions**
    3. Enable events and set the **Request URL**: `https://your-ngrok-url.ngrok-free.app/api/webhook`
    4. Slack will send a challenge — Corsair handles verification automatically
    5. Subscribe to bot events: `message.channels`, `team_join`, `reaction_added`

    **Store the signing secret:**

    ```ts theme={null}
    await corsair.slack.keys.setWebhookSignature(process.env.SLACK_SIGNING_SECRET!);
    ```

    **React to events:**

    ```ts corsair.ts theme={null}
    slack({
        webhookHooks: {
            messages: {
                message: {
                    after: async (ctx, result) => {
                        if (result.data.bot_id) return; // skip bots
                        console.log(`Message: ${result.data.text}`);
                    },
                },
            },
            users: {
                teamJoin: {
                    after: async (ctx, result) => {
                        console.log(`New member: ${result.data.user.name}`);
                    },
                },
            },
        },
    })
    ```
  </Tab>
</Tabs>

***

## How it works

```
Incoming webhook
    → Corsair reads headers to identify the plugin
    → Verifies signature against stored secret
    → Updates local database
    → Runs your webhookHooks.after() handler
```

Every plugin shares the same endpoint. You never write routing logic.

***

## What's next

<CardGroup cols={2}>
  <Card title="Workflows" href="/guides/workflows">
    Chain webhook events into multi-step automations.
  </Card>

  <Card title="Multi-Tenancy" href="/concepts/multi-tenancy">
    Scope incoming webhooks per user with ?tenantId= param.
  </Card>

  <Card title="GitHub Webhooks" href="/plugins/github/webhooks">
    All available GitHub events and payload shapes.
  </Card>

  <Card title="Slack Webhooks" href="/plugins/slack/webhooks">
    All available Slack events and payload shapes.
  </Card>
</CardGroup>
