# Forgejo Webhooks

## When to Use This Skill

* How do I receive Forgejo (or Codeberg) webhooks?
* How do I verify the `X-Forgejo-Signature` header?
* Why is my Forgejo webhook signature verification failing?
* How do I handle `push`, `pull_request`, `issues`, `issue_comment`, `create`, `delete` or `release` events from Forgejo?
* Which header carries the Forgejo event name (`X-Forgejo-Event-Type` vs `X-Forgejo-Event`)?

## Setup note

Set a secret on the Forgejo webhook. Without one, deliveries are unsigned and fail verification. Set the webhook's HTTP method to POST and its content type to application/json. Form-encoded and GET deliveries sign the JSON payload rather than the request body and fail verification.

## Verification (core)

Forgejo signs the **raw request body bytes** with HMAC-SHA256 keyed on the webhook **Secret** (used as UTF-8 bytes, not decoded). `X-Forgejo-Signature` is the bare lowercase hex digest: no `sha256=` prefix, no timestamp, nothing else signed. Compare the whole header value as one digest. If no secret is set, the header is still sent but empty, so reject an empty value.

Node:

```javascript
const crypto = require('crypto');

function verify(rawBody, signature, secret) { // rawBody: Buffer
  if (!secret || typeof signature !== 'string' || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(signature.toLowerCase(), 'hex'), expected);
}

```

Python:

```python
import hashlib, hmac, re

def verify(raw_body: bytes, signature: str | None, secret: str | None) -> bool:
    if not secret or not signature or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(bytes.fromhex(signature), expected)

```

Return **401** for a missing, empty, malformed or mismatched signature. There is no signed timestamp, so there is no replay window to check; use `X-Forgejo-Delivery` for idempotency. Read the raw bytes for any content type (in Express: `express.raw({ type: '*/*' })`) and parse only after verifying.

> **For complete handlers with route wiring, event dispatch, and tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/forgejo-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/forgejo-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/forgejo-webhooks/examples/fastapi/).

## Common Event Types

Dispatch on the **`X-Forgejo-Event-Type`** header (the exact event type, e.g. `issue_assign`). `X-Forgejo-Event` is the coarser group (e.g. `issues`) and is empty for package, schedule and workflow_dispatch events.

| Event type | Description |
| --- | --- |
| `push` | Commits pushed to a branch or tag |
| `pull_request` | Pull request opened, closed, reopened, edited |
| `issues` | Issue opened, closed, reopened, edited |
| `issue_comment` | Comment on an issue |
| `pull_request_comment` | Comment on a pull request (sent with `X-Forgejo-Event: issue_comment`) |
| `create` | Branch or tag created |
| `delete` | Branch or tag deleted |
| `release` | Release published, updated, deleted |

Forgejo documents 41 event types in all, including `fork`, `issue_assign`, `issue_label`, `issue_milestone`, `pull_request_assign`, `pull_request_label` and `pull_request_milestone`. The examples handle the common ones above and log the rest. Most bodies also carry an `action`.

> **Full reference**: [Forgejo webhooks](https://forgejo.org/docs/latest/user/webhooks/)

## Important Headers

| Header | Description |
| --- | --- |
| `X-Forgejo-Signature` | Bare hex HMAC-SHA256 of the raw body (empty if no secret) |
| `X-Forgejo-Event-Type` | Exact event type (`push`, `issue_assign`, ...) |
| `X-Forgejo-Event` | Coarser event group (`push`, `issues`, ...) |
| `X-Forgejo-Delivery` | Unique delivery UUID |
| `X-Hub-Signature-256` | Same digest as `sha256=<hex>` (also sent as `X-Gitea-Signature`, `X-Gogs-Signature`) |

## Environment Variables

```bash
FORGEJO_WEBHOOK_SECRET=your_webhook_secret   # The "Secret" field of the Forgejo webhook

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 forgejo --path /webhooks/forgejo

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/forgejo-webhooks/references/overview.md) - Forgejo webhook concepts and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/forgejo-webhooks/references/setup.md) - Configuration guide
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/forgejo-webhooks/references/verification.md) - Signature verification details