Corsair
Guides

Webhooks

Handling webhooks in Corsair

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:

# Install ngrok — https://ngrok.com/download
ngrok http 3000

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

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 and it'll never change.


Step 2 — Create the webhook endpoint

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

app/api/webhook/route.ts
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

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:

await corsair.github.keys.set_webhook_signature(process.env.GITHUB_WEBHOOK_SECRET!);

React to events:

corsair.ts
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}`);
            },
        },
    },
})

Register in Slack:

  1. Go to 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:

await corsair.slack.keys.setWebhookSignature(process.env.SLACK_SIGNING_SECRET!);

React to events:

corsair.ts
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}`);
                },
            },
        },
    },
})

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