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 3000You'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:
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:
- Go to your repo → Settings → Webhooks → Add webhook
- Set Payload URL:
https://your-ngrok-url.ngrok-free.app/api/webhook - Set Content type:
application/json - Add a Secret — save it
- Choose events (or "Send me everything")
- Click Add webhook
Store the secret:
await corsair.github.keys.set_webhook_signature(process.env.GITHUB_WEBHOOK_SECRET!);React to events:
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:
- Go to api.slack.com/apps → your app
- Navigate to Event Subscriptions
- Enable events and set the Request URL:
https://your-ngrok-url.ngrok-free.app/api/webhook - Slack will send a challenge — Corsair handles verification automatically
- 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:
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() handlerEvery plugin shares the same endpoint. You never write routing logic.