markdown · 3556 bytes Raw Blame History

Webhooks

Webhooks send HTTP POSTs to your URL when something happens in a repo (push, PR opened, issue commented, etc.). Configured at Repository → Settings → Webhooks → "Add webhook".

Configuration

  • Payload URL — the HTTPS endpoint we POST to. HTTP is rejected on production instances.
  • Content typeapplication/json (default) or application/x-www-form-urlencoded.
  • Secret — used to HMAC-sign each delivery. We strongly recommend setting one. The secret is stored AEAD-encrypted at rest; you cannot retrieve it after creation, only replace it.
  • Events — pick "Just push", "Send everything", or specific events.
  • Active — toggle without deleting.

Signature verification

Each delivery includes:

  • X-Shithub-Event: <event-name> — e.g., push, pull_request.
  • X-Shithub-Delivery: <uuid> — unique per delivery (idempotent).
  • X-Shithub-Signature-256: sha256=<hex> — HMAC-SHA256 of the raw body using your configured secret.

Always verify the signature before trusting the payload. Constant-time comparison; never compare with ==.

Go

func verify(body []byte, sig, secret string) bool {
    sig = strings.TrimPrefix(sig, "sha256=")
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Python

import hmac, hashlib

def verify(body: bytes, sig: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    sig = sig.removeprefix("sha256=")
    return hmac.compare_digest(sig, expected)

Node.js

const crypto = require("crypto");

function verify(body, sig, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected),
  );
}

The body must be the raw request body, not the parsed JSON. Frameworks that auto-parse will give you the wrong bytes.

Idempotency

Use X-Shithub-Delivery as your idempotency key. We may retry a delivery if your endpoint returns 5xx or times out, so processing the same delivery twice should be safe in your system.

Retries

A delivery is retried on:

  • Network error.
  • 5xx response.
  • Timeout (default 10s).

Retry schedule: exponential backoff with jitter, up to ~6 retries over ~24h. After 50 consecutive failures, the webhook auto- disables to stop bombarding a broken endpoint. You'll see a banner on the webhook config page; flip "Active" back on once the endpoint is fixed.

Inspecting deliveries

Webhook detail page → "Recent deliveries". Each row shows:

  • Event + delivery ID + timestamp.
  • Request headers + (truncated) body we sent.
  • Response status + headers + (truncated) body we got back.
  • "Redeliver" — re-sends the original payload with the same signature.

Stored bodies are capped at 32 KiB (your endpoint can accept bigger; we just don't keep more for the inspector).

SSRF defense

shithub validates webhook URLs server-side: hostnames are resolved, IPs are checked against a block-list (RFC1918, link- local, loopback, multicast, 169.254.0.0/16, etc.), and the request is dialed to the resolved IP — no following CNAMEs into internal address space at delivery time.

Operators of self-hosted instances can opt in to private destinations via an AllowedHosts list — see self-host configuration.

View source
1 # Webhooks
2
3 Webhooks send HTTP POSTs to your URL when something happens in a
4 repo (push, PR opened, issue commented, etc.). Configured at
5 Repository → Settings → Webhooks → "Add webhook".
6
7 ## Configuration
8
9 - **Payload URL** — the HTTPS endpoint we POST to. HTTP is
10 rejected on production instances.
11 - **Content type** — `application/json` (default) or
12 `application/x-www-form-urlencoded`.
13 - **Secret** — used to HMAC-sign each delivery. We strongly
14 recommend setting one. The secret is stored AEAD-encrypted at
15 rest; you cannot retrieve it after creation, only replace it.
16 - **Events** — pick "Just push", "Send everything", or specific
17 events.
18 - **Active** — toggle without deleting.
19
20 ## Signature verification
21
22 Each delivery includes:
23
24 - `X-Shithub-Event: <event-name>` — e.g., `push`, `pull_request`.
25 - `X-Shithub-Delivery: <uuid>` — unique per delivery (idempotent).
26 - `X-Shithub-Signature-256: sha256=<hex>` — HMAC-SHA256 of the
27 raw body using your configured secret.
28
29 **Always verify the signature before trusting the payload.**
30 Constant-time comparison; never compare with `==`.
31
32 ### Go
33
34 ```go
35 func verify(body []byte, sig, secret string) bool {
36 sig = strings.TrimPrefix(sig, "sha256=")
37 mac := hmac.New(sha256.New, []byte(secret))
38 mac.Write(body)
39 expected := hex.EncodeToString(mac.Sum(nil))
40 return hmac.Equal([]byte(sig), []byte(expected))
41 }
42 ```
43
44 ### Python
45
46 ```python
47 import hmac, hashlib
48
49 def verify(body: bytes, sig: str, secret: str) -> bool:
50 expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
51 sig = sig.removeprefix("sha256=")
52 return hmac.compare_digest(sig, expected)
53 ```
54
55 ### Node.js
56
57 ```js
58 const crypto = require("crypto");
59
60 function verify(body, sig, secret) {
61 const expected = "sha256=" + crypto
62 .createHmac("sha256", secret)
63 .update(body)
64 .digest("hex");
65 return crypto.timingSafeEqual(
66 Buffer.from(sig),
67 Buffer.from(expected),
68 );
69 }
70 ```
71
72 The body must be the **raw request body**, not the parsed JSON.
73 Frameworks that auto-parse will give you the wrong bytes.
74
75 ## Idempotency
76
77 Use `X-Shithub-Delivery` as your idempotency key. We may retry a
78 delivery if your endpoint returns 5xx or times out, so processing
79 the same delivery twice should be safe in your system.
80
81 ## Retries
82
83 A delivery is retried on:
84
85 - Network error.
86 - 5xx response.
87 - Timeout (default 10s).
88
89 Retry schedule: exponential backoff with jitter, up to ~6 retries
90 over ~24h. After 50 consecutive failures, the webhook **auto-
91 disables** to stop bombarding a broken endpoint. You'll see a
92 banner on the webhook config page; flip "Active" back on once
93 the endpoint is fixed.
94
95 ## Inspecting deliveries
96
97 Webhook detail page → "Recent deliveries". Each row shows:
98
99 - Event + delivery ID + timestamp.
100 - Request headers + (truncated) body we sent.
101 - Response status + headers + (truncated) body we got back.
102 - "Redeliver" — re-sends the original payload with the same
103 signature.
104
105 Stored bodies are capped at 32 KiB (your endpoint can accept
106 bigger; we just don't keep more for the inspector).
107
108 ## SSRF defense
109
110 shithub validates webhook URLs server-side: hostnames are
111 resolved, IPs are checked against a block-list (RFC1918, link-
112 local, loopback, multicast, 169.254.0.0/16, etc.), and the
113 request is dialed to the resolved IP — no following CNAMEs into
114 internal address space at delivery time.
115
116 Operators of self-hosted instances can opt in to private
117 destinations via an `AllowedHosts` list — see
118 [self-host configuration](../self-host/configuration.md).