Skip to main content
Webhooks, explained

How Webhooks Work Rebuild GitLab Webhooks to Find Out

A webhook is one HTTP POST your server sends when something happens. That is the whole idea. The fastest way to really get it is to rebuild a webhook system you already use every day, so this page rebuilds GitLab's push webhook in about 40 lines, then shows what a production system adds on top.

Trusted by teams at

Coinbase Eudonet GEODIS WoodWing Optery Alteos ActiveAnts Apizee
The idea

A Webhook Is a Reverse API Call

With a normal API, your code calls someone else and waits. With a webhook, they call you.

You register a URL with a provider. When an event fires on their side, a new commit, a paid invoice, a closed merge request, they send an HTTP POST to that URL with a JSON body describing what happened. No polling, no waiting, no cron job hammering an endpoint every 30 seconds asking "anything new yet?". The event pushes itself to you the moment it exists. GitLab, Stripe, GitHub, and Shopify all work this way. Under the hood it is the same three moves every time: an event happens, the provider builds a payload, the provider POSTs it to your URL.

Rebuild it

Rebuild GitLab's Push Webhook

GitLab's own webhooks are a good teacher because the mechanics are visible and small. Here is the sender, the part GitLab runs when you push.

1

An event happens

A developer pushes commits. GitLab now has a fact to broadcast: repository X received a push on branch Y. Your job as the sender is to turn that fact into an HTTP request.

2

Build the payload

Serialize the event to JSON: the ref that changed, the commits, the author, the project. GitLab uses one shape per event type, so a receiver can branch on the event name and trust the fields that follow.

3

Sign and POST it

GitLab sets two headers a receiver checks: X-Gitlab-Event names the event ("Push Hook"), and X-Gitlab-Token carries the shared secret you configured. Then it POSTs the JSON to your subscriber URL. That request is the webhook.

The sender (what GitLab runs)

// Fire a webhook when a push happens. This is the whole core.
async function sendPushWebhook(subscriber, push) {
  const payload = {
    object_kind: 'push',
    ref: push.ref,                 // refs/heads/main
    checkout_sha: push.sha,
    user_username: push.author,
    project: { name: push.project, web_url: push.url },
    commits: push.commits,
  };

  await fetch(subscriber.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Gitlab-Event': 'Push Hook',
      'X-Gitlab-Token': subscriber.secretToken,
    },
    body: JSON.stringify(payload),
  });
}

GitLab authenticates with a plain shared token in X-Gitlab-Token, so the receiver compares strings. GitHub and Stripe instead sign the body with HMAC-SHA256 and send the digest in a header (X-Hub-Signature-256 for GitHub), which is stronger because the signature is bound to the exact bytes of the payload.

The receiver (what you run)

// Verify the token, then act on the event.
app.post('/webhooks/gitlab', (req, res) => {
  const token = req.header('X-Gitlab-Token');
  if (token !== process.env.WEBHOOK_SECRET) {
    return res.status(401).send('bad token');
  }

  const event = req.header('X-Gitlab-Event'); // 'Push Hook'
  if (event === 'Push Hook') {
    deployBranch(req.body.ref, req.body.checkout_sha);
  }

  res.status(200).send('ok'); // ack fast, work async
});

That is a working webhook end to end. It is also where the easy part ends: the code above assumes the network never fails, the receiver is always up, and nobody replays an old request.

The hard 20%

What Production Delivery Adds

The 40-line version works on a whiteboard. Real traffic breaks it in ways you only see at 3am.

Retries with backoff

The receiver returns a 500 or times out. Do you drop the event? Retry immediately and hammer a service that is already struggling? You need a retry schedule with increasing delays, a cap on attempts, and jitter so every failed delivery does not retry in lockstep.

Signatures done right

A shared token in a header leaks the moment it lands in a log. HMAC over the raw body plus a timestamp lets the receiver verify the bytes came from you and are recent, which kills replay attacks. Now you own key storage and rotation.

Idempotency

A retry means the same event can arrive twice. Send a stable event id so the receiver can dedupe, or you will double-charge a card and double-deploy a branch.

Delivery visibility

Your first integrator will ask "did my webhook fire?" on day one. You need a log of every attempt, the response code, the latency, and a button to replay a failed delivery. Building that dashboard is its own project.

Ordering and speed

Push events can arrive out of order, and a slow subscriber must not block every other delivery. That means a queue, per-subscriber concurrency, and a fast path that acks before the heavy work runs.

Subscriber management

Endpoint registration, URL validation, event-type filtering, disabling dead endpoints, and a portal your users can self-serve. None of this is the fun part, and all of it is required.

The gap

Prototype vs Production

Concern 40-line prototype Production system
Delivery on failure Event is lost Retried on a backoff schedule
Auth Plain shared token HMAC signature plus timestamp
Duplicate events Not handled Stable event id for dedupe
Debugging Read server logs by hand Delivery log with replay
Slow subscriber Blocks the sender Queued, per-subscriber concurrency
Onboarding a subscriber Edit code, redeploy Self-serve portal and API
Skip the plumbing

Hook0 Is the Production Half, as a Service

You just saw the 40 lines that fire a webhook, and the six hard problems that turn those 40 lines into a six-month project. Hook0 is an open-source webhook platform that owns the hard half: you POST an event once to the API, and Hook0 signs it, retries it on a configurable schedule, logs every attempt, and gives your subscribers a portal. Self-host the SSPL code or use the EU-hosted cloud. 100 events per day are free, no credit card.

Send an event, Hook0 delivers it

curl -X POST https://app.hook0.com/api/v1/event \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "push.received",
    "payload": { "ref": "refs/heads/main", "sha": "a1b2c3" }
  }'

Retries, HMAC signatures, delivery logs, and subscriber notification are handled for you.

FAQ

Webhook Questions, Answered

What is a webhook in simple terms?

A webhook is an automated HTTP POST that one server sends to another when a specific event happens. Instead of your app repeatedly asking an API "anything new?", the provider pushes the event to a URL you registered, the moment it occurs.

How do GitLab webhooks work?

When an event fires (a push, a merge request, a pipeline change), GitLab sends a POST to your configured URL with a JSON body. It sets X-Gitlab-Event to name the event and X-Gitlab-Token to carry the secret you set, so your endpoint can verify the request and branch on the event type.

What is the difference between a webhook and an API?

Direction. With an API, your code calls the provider and waits for a response. With a webhook, the provider calls your code when something happens. An API is pull, a webhook is push. Most integrations use both.

How do you secure a webhook endpoint?

Verify every request before you act on it. GitLab uses a shared token; GitHub and Stripe sign the raw body with HMAC-SHA256 and send the digest in a header, which also defends against replay when paired with a timestamp. Always serve the endpoint over HTTPS and return a fast 2xx to acknowledge receipt.

Why not just build webhooks yourself?

Sending the POST is 40 lines. Retries with backoff, HMAC signatures with key rotation, idempotency, delivery logs, queuing, and a subscriber portal are the other six months. A webhook platform like Hook0 gives you that half as a service, open-source and self-hostable if you want to keep it in-house.

You have better things to build

Stop building webhook infrastructure. Start shipping features. Get started in minutes.

No credit card required
Setup in 5 minutes
Cancel anytime