Public Beta: Direct engineering support available. Join Discord →
Docs
Recipes
GitHub: Debug & Route Webhooks

Debug and Route GitHub Webhooks

Time: ~10 minutes | Difficulty: Beginner | Prerequisites: Admin access to a GitHub repository or organization, Slashbin account

TL;DR: GitHub's Recent Deliveries panel shows you a status code and the payload it sent. It does not show what your handler did with it, and its Redeliver button re-runs against whatever your system looks like now. Route GitHub through Slashbin and every push, pull request and workflow event is stored with its payload, transform result, and every delivery attempt — replayable from storage.


The Problem

A workflow_run event should have kicked off your deployment tracker. It didn't. You open the repository's webhook settings, click into Recent Deliveries, and find one line: 500.

That line is the entire diagnostic. GitHub records what it sent and what status came back. It has no visibility into anything past your endpoint's front door:

  • Which stage failed? A signature mismatch, a parser exception, or a handler that returned 500 after already doing half the work.
  • What did your code actually produce? GitHub shows its own payload, never your transformed output.
  • What happens when you click Redeliver? GitHub re-sends the same payload against your current state. If the handler is not idempotent, you get a second side effect. If the state has moved on, the replay no longer reproduces the original failure.
  • What about the other 40 events? Recent Deliveries is per-webhook and short-lived. There's no cross-repo history to search.

And GitHub webhooks arrive in volume. A busy org sends push, pull_request, check_run, and workflow_run events continuously — a handler that silently drops a fraction of them looks healthy right up until someone asks why a specific deployment was never recorded.


The Solution

Route GitHub through a Slashbin project. Verification happens at the gateway from stored configuration, and every event is recorded end-to-end:

  • The raw payload exactly as GitHub sent it — bytes and headers, including x-github-delivery.
  • The transform result — what your Golden Model produced, or the error it threw.
  • Every delivery attempt to your endpoint — response code, response body, and timing.
  • The final disposition — delivered, retrying, or in the DLQ.

Outcome: You stop debugging from a status code. The event that broke is on disk, attributable to a stage, and replayable without touching GitHub.


Step 1: Create a GitHub Project in Slashbin

  1. Click Create New Project and select GitHub.
  2. Type: Choose Transactional — repository and workflow events are discrete business events, not a stream of metrics.
  3. HMAC Signing Secret — enter the secret you will paste into GitHub in Step 2. Generate a strong random string; GitHub does not create one for you.

Selecting GitHub loads its verification contract into the project:

FieldGitHub's value
Auth typeHMAC
Signature headerx-hub-signature-256
Signature encodinghex
Signature prefixsha256=
Signed payloadthe raw request body
Event topicx-github-event, read from the header
Delivery idx-github-delivery

Slashbin's gateway has no GitHub-specific code path. GitHub works because that contract is stored and applied on every request — the same mechanism that verifies Stripe, Square and Shopify.

⚠️

The sha256= prefix is part of the header value, not a description of it. x-hub-signature-256 carries sha256= followed by the hex digest. Providers that use the same prefix over a base64 digest — Typeform is one — produce a mismatch that looks identical to a wrong key. This is the single most common reason a hand-rolled GitHub check fails.


Step 2: Add the Slashbin Ingestion URL as a Repository or Org Webhook

  1. Copy the Ingestion URL from the Slashbin project dashboard.
  2. In GitHub, open Settings → Webhooks → Add webhook — on a repository for one repo, or on the organization to cover all of them.
  3. Payload URL: paste the Slashbin Ingestion URL.
  4. Content type: select application/json.
  5. Secret: paste the same secret you entered in Step 1.
  6. Which events? Choose Let me select individual events and pick what you actually consume — Pushes, Pull requests, Workflow runs. Selecting everything works, but you will pay for it in noise.
  7. Save.

GitHub immediately sends a ping event. Open the Slashbin delivery log; a verified ping confirms the secret matches on both sides before you wire up anything downstream.


Step 3: Route Events by Type

Here is where GitHub differs from most providers you have integrated.

Stripe and Square put the event type in the body. Stripe sends "type": "invoice.paid"; Square sends a top-level type field. Routing rules for those sources read a JSON path.

GitHub puts it in a header. The body of a pull_request event never names itself — you have to read x-github-event. Two events with completely different meanings can have bodies that look structurally similar; the header is the authoritative discriminator.

A GitHub delivery arrives looking like this:

{
  "headers": {
    "x-github-event": "workflow_run",
    "x-github-delivery": "9f4a1e60-5c2b-11f0-9e3a-1f0d2c8b7a41",
    "x-hub-signature-256": "sha256=c8b7a41f0d2c9e3a11f05c2b9f4a1e60...",
    "content-type": "application/json"
  },
  "body": {
    "action": "completed",
    "workflow_run": {
      "id": 10482913746,
      "name": "deploy",
      "head_branch": "main",
      "status": "completed",
      "conclusion": "failure",
      "run_number": 412
    },
    "repository": {
      "full_name": "slashbin-io/example-service"
    }
  }
}

Three things worth reading off that record:

  • The topic is workflow_run, from the header. Not action, not workflow_run.name. Write your routing rules against x-github-event.
  • action narrows the topic, it does not replace it. workflow_run + completed is a different rule from workflow_run + requested. A rule that matches only on action: "completed" will also catch check_run and pull_request completions.
  • x-github-delivery is the id worth carrying through. It is the per-delivery identifier GitHub shows in Recent Deliveries and quotes in support conversations. Map it into your Golden Model and a single string will join your logs, Slashbin's delivery log, and GitHub's own record of the same event.

With rules written against the header, one webhook endpoint can fan out cleanly: push to your build tracker, pull_request to your review bot, workflow_run to your deployment dashboard — each with its own retry policy and its own destination.

An event type you never wrote a rule for is not silently discarded — it is recorded as unmatched, so you can see what your webhook is actually sending. Selecting "send me everything" in GitHub and then routing nothing is the fastest way to fill that list.


Step 4: Debug and Replay From Stored Evidence

When something doesn't arrive downstream, open the project's Delivery Log and find the event — by x-github-delivery, by timestamp, or by filtering to failed deliveries. Walk the stored record top-down:

  • Signature verification failed → The event was rejected at the gateway. The secret in GitHub does not match the one in the project. Check for a trailing newline pasted along with the value before you regenerate anything.
  • Transform errored → Your Golden Model threw. The record carries the error and the source field it choked on. GitHub payloads vary by event type more than most; a mapping written against pull_request will not survive a push.
  • Attempt returned 4xx → Your endpoint rejected the payload. The stored response body is exactly what your handler returned.
  • Attempt returned 5xx or timed out → Transient failure. Slashbin retries with backoff; when attempts exhaust, the event lands in the Dead Letter Queue rather than disappearing.
  • Delivered, but nothing happened downstream → Delivery succeeded end-to-end and your handler returned 2xx. The bug is inside your own code, past the point Slashbin can see.

Once the fix is deployed, replay from the Dead Letter Queue. The payload comes from Slashbin's storage, not from GitHub — which matters, because GitHub's own Redeliver re-runs against current state and is only available while the delivery is still in the panel's retention window.


Why Slashbin vs a Raw Handler

  • The signature contract is configuration, not crypto in your app. Hex-encoded HMAC-SHA256 over the raw body, sha256= prefix, x-hub-signature-256 — applied at the gateway, so your handler never carries provider-specific verification code that quietly breaks when you add the next source.
  • Header-based routing without header-parsing code. x-github-event becomes a routing rule, not an if-else chain at the top of your controller.
  • Debug from stored evidence. The exact payload that broke, the transform result, and every attempt with its response body — instead of a status code in a panel that ages out.
  • Replay from storage, not from GitHub. The original event is re-runnable after a fix, whether or not GitHub still has it.
  • Unmatched events surface instead of vanishing. An event type nothing routes is a visible record you can act on, not a silent drop.

Next Steps