Skip to content
repo-sdk
Esc
navigateopen⌘Jpreview
On this page

Receiving webhooks

Verify webhook signatures and parse incoming events with the standalone helpers.

verifyWebhook and parseWebhookEvent are standalone exports on each provider subpath — no client needed. They accept a Web-standard Request (or a { headers, body } pair), so they drop straight into Next.js route handlers, Hono, and workers.

Import from the provider subpath

Both helpers live on each provider’s subpath — import the pair that matches the provider sending the webhook:

import { verifyWebhook, parseWebhookEvent } from 'repo-sdk/github';
// or 'repo-sdk/gitlab' | 'repo-sdk/bitbucket' | 'repo-sdk/azure-devops' | 'repo-sdk/gitea'

A full handler

The helpers accept a Web-standard Request, so one handler works across Next.js route handlers, Hono, and Cloudflare Workers:

import { verifyWebhook, parseWebhookEvent } from 'repo-sdk/github';

export async function POST(request: Request): Promise<Response> {
  const valid = await verifyWebhook({ request, secret: process.env.WEBHOOK_SECRET! });
  if (!valid) return new Response('invalid signature', { status: 401 });

  const event = await parseWebhookEvent(request);
  switch (event.type) {
    case 'push':
      console.log(`push to ${event.repo} @ ${event.ref}`, event.commits);
      break;
    case 'tag_push':
      console.log(`tag push to ${event.repo} @ ${event.ref}`);
      break;
    case 'release':
      console.log(`release in ${event.repo}`);
      break;
    case 'ping':
      break; // GitHub sends a ping when a hook is created
  }
  return new Response('ok');
}

One endpoint for multiple providers

When a single endpoint receives webhooks from more than one provider, detectWebhookProvider (a standalone export from the core repo-sdk entry) identifies the sender so you can dispatch to the right per-provider helpers:

import { detectWebhookProvider, type ProviderName } from 'repo-sdk';
import * as github from 'repo-sdk/github';
import * as gitlab from 'repo-sdk/gitlab';

type WebhookHelpers = Pick<typeof github, 'verifyWebhook' | 'parseWebhookEvent'>;
const handlers: Partial<Record<ProviderName, WebhookHelpers>> = { github, gitlab };

export async function POST(request: Request): Promise<Response> {
  const provider = await detectWebhookProvider(request);
  const helpers = provider === undefined ? undefined : handlers[provider];
  if (helpers === undefined) return new Response('unknown sender', { status: 400 });

  const { verifyWebhook, parseWebhookEvent } = helpers;
  // ... verify and parse with the matched pair
}

It accepts a Request or { headers, body? } and returns the ProviderName or undefined. GitHub, GitLab, Bitbucket, and Gitea are identified by their event headers (Gitea deliveries also carry a compatibility x-github-event header — the helper handles that); Azure DevOps sends no identifying header, so its JSON body is checked for the eventType field, which is why the helper is async.

Verification per provider

verifyWebhook handles each provider’s scheme transparently — you always call it the same way. It returns false (never throws) on a missing or mismatched signature, and comparisons are constant-time.

Provider Method How it verifies
GitHub HMAC-SHA256 x-hub-signature-256 over the raw body
GitLab shared token x-gitlab-token compared to the configured secret
Bitbucket HMAC-SHA256 x-hub-signature (no -256 suffix) over the raw body
Azure DevOps basic auth Authorization: Basic header (hook registered with the secret)
Gitea HMAC-SHA256 x-gitea-signature over the raw body (bare hex, no sha256= prefix)
PropType
request?Request

A Web-standard Request. Provide this or headers + body.

TypeRequest
headers?Record<string, string>

Header map, when you pass the body separately.

TypeRecord<string, string>
body?string

The raw request body — required with headers.

Typestring
secret?string

The secret the hook was registered with.

Typestring

The parsed event

parseWebhookEvent normalizes the payload into a ParsedWebhookEvent, regardless of provider:

PropType
type?'push' | 'tag_push' | 'release' | 'ping' | 'unknown'

The normalized event type. Unrecognized events map to unknown.

Type'push' | 'tag_push' | 'release' | 'ping' | 'unknown'
repo?string

The repository path, when present in the payload.

Typestring
ref?string

The git ref (e.g. refs/heads/main or refs/tags/v1.0.0).

Typestring
commits?{ sha: string; message?: string }[]

The commits in the delivery, when present.

Type{ sha: string; message?: string }[]
headCommitSha?string

The SHA the ref points to after the push. Undefined when the push deleted the ref.

Typestring
deliveryId?string

The provider's delivery id, for de-duplication/logging.

Typestring
webhookId?string

The provider-assigned id of the webhook registration that produced the delivery.

Typestring
raw?unknown

The untouched provider payload.

Typeunknown

Reference: Provider support

The full feature-by-provider matrix, including webhooks.

Was this page helpful?