---
title: "Receive webhooks"
description: "Verify a delivery and turn it into normalised events."
canonical_url: "https://forges.link/guides/webhooks"
---
# Receive webhooks

> Verify a delivery and turn it into normalised events.

A webhook is how a forge tells your server that something happened, such as a new comment or a merged pull request. The forge sends an HTTP request to a URL you choose, and every forge signs and formats that request in its own way. `webhooks.ingest()` takes care of this for you. It checks the signature against your `webhookSecret` and translates the delivery into `ForgeEvent` objects that look the same on every forge. If the signature doesn't match, it throws `WebhookVerificationError`, so a request that didn't come from the forge never reaches your code.

## Write a handler

A forge signs the exact bytes of the request body that it sends. This means you have to pass the raw body to `ingest()`. If you parse the body and serialise it again, the bytes can change, and the signature check fails:

```ts
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 is built on the Fetch API, so it runs on any runtime that supports it. A single delivery can describe more than one event, which is why `ingest()` returns an array rather than one event.

If you only want to know whether a delivery is authentic, call `webhooks.verify()` to check the signature without translating the payload. And when a delivery needs a different secret from the one the provider was created with, pass it as `secret` next to `headers` and `body`.

## Handle one forge or many

When you receive deliveries from several forges, register them all with `createForges()` and use the route to decide which provider should read a request. The [label bot](https://forges.link/examples/label-bot) does this by serving each forge on `/webhooks/<forge>`:

```ts
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`. When an event concerns a thread or a repository, `thread` and `repo` hold the refs for it. The `detail` field carries typed fields that depend on the kind, so you can narrow on `detail.type` and get a typed result:

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

If you need something that the normalised event doesn't expose, `event.payload` still holds the unnormalised delivery. For the full model, see [Events](https://forges.link/concepts/events).

## Register a webhook

Receiving deliveries requires a webhook to exist on the forge. If the provider supports it, you can create one from code instead of through the forge's settings. Pass a repository ref, or a namespace ref if you want to hook a whole organisation:

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

The `events` option takes the normalised kinds. If you also need events that have no normalised kind, add the forge's own event names with `nativeEvents`, and the provider combines both lists. To see which kinds and actions a provider can deliver, read `forge.webhooks.events`.

## Manage webhooks from a browser

A browser can't receive deliveries, so the Lite providers that you use in browser code don't include webhook ingestion, and their `capabilities.sources.webhook` is `false`. Managing webhooks with `create()` and `list()` still works there. For more on the Lite providers, see [Bundling](https://forges.link/guides/bundling).

The headers that a forge sends and the way it signs a delivery differ from forge to forge. Each [provider page](https://forges.link/providers) lists them.


## Sitemap

See the full [sitemap](https://forges.link/sitemap.md) for all pages.
