Guides

Receive webhooks

Verify a delivery and turn it into normalised events.

webhooks.ingest() verifies a delivery against webhookSecret and translates it into ForgeEvent objects. If the signature doesn't match, it throws WebhookVerificationError.

Write a handler

The forge signs the exact bytes it sends, so pass the raw body. Don't parse and re-serialise it:

import { WebhookVerificationError } from 'forges'
import { github } from 'forges/github'

const forge = github({
  webhookSecret: process.env.GITHUB_WEBHOOK_SECRET,
}).create()

export default async function handler(request: Request) {
  try {
    const events = await forge.webhooks.ingest({
      headers: request.headers,
      body: await request.text(),
    })
    for (const event of events) {
      console.log(event.kind, event.action, event.summary)
    }
    return new Response(null, { status: 204 })
  }
  catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response('Invalid signature', { status: 401 })
    }
    throw error
  }
}

The handler uses the Fetch API, so it runs on any runtime that supports it. A delivery can produce more than one event, which is why ingest() returns an array.

To check the signature without translating the delivery, call webhooks.verify(). To use a secret for one delivery only, pass secret next to headers and body.

Handle one forge or many

With several forges, register them all and pick the provider from the route. The label bot serves /webhooks/<forge> this way:

const provider = forges.get('gitlab')
const events = await provider?.webhooks.ingest({
  headers: request.headers,
  body: await request.text(),
})

Act on events

Every event has a kind, such as comment, review or push, and an action, such as created, closed or merged. thread and repo hold refs when the event concerns one, and detail has typed fields for the kind:

for (const event of events) {
  if (
    event.kind === 'comment'
    && event.detail?.type === 'comment'
    && event.thread
  ) {
    console.log(`${event.actor?.login} commented: ${event.detail.body}`)
  }
}

event.payload keeps the unnormalised delivery. See Events for the full model.

Register a webhook

If the provider supports it, you can create the hook from code. Pass a repository ref, or a namespace ref to hook an organisation:

if (forge.can('webhooks.create')) {
  await forge.webhooks.create(repo, {
    url: 'https://example.com/webhooks/github',
    events: ['comment', 'state_change'],
    secret: process.env.GITHUB_WEBHOOK_SECRET,
  })
}

events takes normalised kinds. nativeEvents takes the forge's own event names and merges with them. forge.webhooks.events lists the kinds and actions a provider can deliver.

Manage webhooks from a browser

A browser doesn't receive deliveries, so a Lite provider has no webhook ingestion, and its capabilities.sources.webhook is false. Webhook management with create() and list() still works. See Browser bundles.

Each provider page lists the headers that forge sends and how it signs a delivery.

Copyright © 2026