Webhook Debugging Guide
GitHub webhook: "We couldn't deliver this payload"
Short answer
In a repo or org's Settings → Webhooks → Recent Deliveries, GitHub shows this when your endpoint didn't answer successfully:
We couldn't deliver this payload: Service Unavailable
or a variant ending in timed out. GitHub gives your server a short window, on the order of ten seconds, to respond with a 2xx status. If your server is down, slow, or behind something that isn't listening on that URL yet, delivery fails and GitHub marks it red.
Read the actual response
Click a failed delivery to expand it: GitHub shows the exact request it sent (headers and body) and the response it got back, or the connection error if there wasn't one. That's usually enough to tell a wrong URL from a slow handler from a server that isn't running.
Once you've fixed it, click Redeliver on that same delivery to resend the identical payload. GitHub doesn't sign with a timestamp, so a redelivered payload from a week ago verifies exactly the same as one from a second ago — unlike Stripe, Polar or Slack.
Testing locally: gh webhook forward
The GitHub CLI can stream a repo's webhook events straight to your machine while it's running:
gh webhook forward \
--repo owner/repo \
--events push,pull_request \
--url http://localhost:3000/webhook-receiver
It needs the admin:repo_hook scope and creates a temporary webhook for the session. Like other local-forwarding CLIs, it only delivers while it's running on that machine, and it doesn't tell you why a signature check failed if you're also verifying X-Hub-Signature-256.
Verifying the signature yourself
If a secret is configured on the webhook, GitHub sends X-Hub-Signature-256: sha256=<hex hmac>, an HMAC-SHA256 of the raw body keyed with that secret. There's no timestamp involved, so this check either matches or it doesn't — a wrong secret is the only real cause of a mismatch here.
The easier way: WebhookMon
WebhookMon's relay URL doesn't expire and doesn't need gh webhook forward running to keep events flowing. It shows the raw request and response for every delivery, forwards to your local server automatically, and replays any event on demand without waiting for GitHub's own Redeliver.