---
title: "Receive webhooks"
description: "Verify a delivery and turn it into normalised events."
canonical_url: "https://docs-8831a2g1f-poetry.vercel.app/guides/webhooks"
---
# 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:

```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 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](https://docs-8831a2g1f-poetry.vercel.app/examples/label-bot) serves `/webhooks/<forge>` this way:

```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`. `thread` and `repo` hold refs when the event concerns one, and `detail` has typed fields for the kind:

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

`event.payload` keeps the unnormalised delivery. See [Events](https://docs-8831a2g1f-poetry.vercel.app/concepts/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:

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

`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](https://docs-8831a2g1f-poetry.vercel.app/guides/bundling).

Each [provider page](https://docs-8831a2g1f-poetry.vercel.app/providers) lists the headers that forge sends and how it signs a delivery.


## Sitemap

See the full [sitemap](https://docs-8831a2g1f-poetry.vercel.app/sitemap.md) for all pages.
