Skip to content
githome

Webhooks

create and manage repository webhooks and verify delivery signatures

Webhooks send HTTP POST requests to a URL you control whenever events occur in a repository. Githome mirrors the GitHub webhooks API including delivery history, redelivery, and HMAC-SHA256 signature verification.

Create a webhook

POST /repos/{owner}/{repo}/hooks
curl -X POST \
  -H "Authorization: Bearer TOKEN" \
  -H "Accept: application/vnd.github+json" \
  -H "Content-Type: application/json" \
  http://localhost:3000/repos/alice/myrepo/hooks \
  -d '{
    "config": {
      "url": "https://hooks.example.com/githome",
      "content_type": "json",
      "secret": "s3cr3t",
      "insecure_ssl": "0"
    },
    "events": ["push", "pull_request", "issues"],
    "active": true
  }'

Response (201 Created):

{
  "id": 7,
  "name": "web",
  "active": true,
  "events": ["push", "pull_request", "issues"],
  "config": {
    "url": "https://hooks.example.com/githome",
    "content_type": "json",
    "insecure_ssl": "0"
  },
  "created_at": "2026-06-10T08:00:00Z",
  "updated_at": "2026-06-10T08:00:00Z"
}

The secret field is write-only; it never appears in GET responses. Set insecure_ssl to "1" only for development; githome will skip TLS certificate validation when delivering to that hook.

Supported events

Event name Triggered when
push Commits pushed to any branch
pull_request PR opened, closed, merged, edited, synchronized, labeled, assigned, review requested
pull_request_review Review submitted, dismissed, or edited
pull_request_review_comment Inline review comment created, edited, or deleted
issues Issue opened, closed, edited, labeled, assigned, milestoned
issue_comment Comment created, edited, or deleted on an issue or PR
create Branch or tag created
delete Branch or tag deleted
release Release published, created, edited, or deleted
ping Sent when a webhook is first created or when you call the ping endpoint

Use "*" in the events array to subscribe to all events.

List, get, and update webhooks

List all webhooks on a repository:

curl -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks

Get a single webhook:

curl -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/7

Update events or config:

curl -X PATCH \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  http://localhost:3000/repos/alice/myrepo/hooks/7 \
  -d '{
    "events": ["push", "pull_request", "issues", "release"],
    "active": true,
    "config": {
      "url": "https://hooks.example.com/githome-v2"
    }
  }'

Only fields present in the request body are updated. Omitting config leaves the existing config unchanged.

Delete a webhook

curl -X DELETE \
  -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/7

Returns 204 No Content on success.

Ping a webhook

Send a test ping delivery without triggering a real event:

curl -X POST \
  -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/7/pings

Returns 204. The delivery appears in the delivery history with event ping.

Delivery history

List recent deliveries for a webhook:

curl -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/7/deliveries
[
  {
    "id": 301,
    "guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "delivered_at": "2026-06-10T09:15:00Z",
    "redelivery": false,
    "duration": 0.231,
    "status": "OK",
    "status_code": 200,
    "event": "push",
    "action": null,
    "url": "https://hooks.example.com/githome"
  }
]

Get the full request and response for one delivery:

curl -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/deliveries/301

The response includes request.headers, request.payload, response.headers, and response.payload fields with the raw content.

Redeliver a delivery

curl -X POST \
  -H "Authorization: Bearer TOKEN" \
  http://localhost:3000/repos/alice/myrepo/hooks/deliveries/301/attempts

Returns 202 Accepted. The redelivery appears as a new entry in the delivery list with redelivery: true.

Signature verification

Every delivery carries these headers:

Header Value
X-GitHub-Event Event name, e.g. push
X-GitHub-Delivery UUID identifying this delivery
X-GitHub-Hook-ID Numeric webhook ID
X-Hub-Signature-256 sha256=<hex digest>

The signature is HMAC-SHA256 of the raw request body using the hook secret as the key. Always verify before processing the payload.

Python example

import hashlib
import hmac

def verify_signature(secret: str, body: bytes, signature_header: str) -> bool:
    if not signature_header.startswith("sha256="):
        return False
    expected = hmac.new(
        secret.encode(),
        msg=body,
        digestmod=hashlib.sha256,
    ).hexdigest()
    received = signature_header[len("sha256="):]
    return hmac.compare_digest(expected, received)

# In your request handler:
# body = request.get_data()  # raw bytes, before JSON parsing
# ok = verify_signature("s3cr3t", body, request.headers["X-Hub-Signature-256"])

Go example

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "net/http"
    "strings"
)

func verifySignature(secret string, body []byte, sigHeader string) error {
    if !strings.HasPrefix(sigHeader, "sha256=") {
        return errors.New("missing sha256= prefix")
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    received := sigHeader[len("sha256="):]
    if !hmac.Equal([]byte(expected), []byte(received)) {
        return errors.New("signature mismatch")
    }
    return nil
}

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    if err := verifySignature("s3cr3t", body, r.Header.Get("X-Hub-Signature-256")); err != nil {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }
    // safe to parse body now
}

Always read the full raw body before parsing JSON. Parsers may normalize whitespace, changing the byte sequence and breaking signature verification.

Payload examples

push payload

{
  "ref": "refs/heads/main",
  "before": "abc123",
  "after": "def456",
  "repository": {
    "id": 12,
    "name": "myrepo",
    "full_name": "alice/myrepo",
    "private": false,
    "owner": { "login": "alice" }
  },
  "pusher": { "name": "alice", "email": "[email protected]" },
  "commits": [
    {
      "id": "def456",
      "message": "fix: handle nil pointer",
      "timestamp": "2026-06-10T08:00:00Z",
      "url": "http://localhost:3000/alice/myrepo/commit/def456",
      "author": { "name": "Alice", "email": "[email protected]", "username": "alice" },
      "added": ["src/handler.go"],
      "removed": [],
      "modified": ["src/config.go"]
    }
  ],
  "head_commit": { "id": "def456", "message": "fix: handle nil pointer" }
}

pull_request payload

{
  "action": "opened",
  "number": 42,
  "pull_request": {
    "id": 200,
    "number": 42,
    "state": "open",
    "title": "Add rate limiting",
    "body": "Implements token bucket rate limiting per user.",
    "user": { "login": "bob" },
    "head": {
      "ref": "feature/rate-limit",
      "sha": "aaa111",
      "repo": { "full_name": "bob/myrepo" }
    },
    "base": {
      "ref": "main",
      "sha": "bbb222",
      "repo": { "full_name": "alice/myrepo" }
    },
    "merged": false,
    "draft": false,
    "created_at": "2026-06-10T09:00:00Z",
    "updated_at": "2026-06-10T09:00:00Z"
  },
  "repository": {
    "id": 12,
    "full_name": "alice/myrepo"
  },
  "sender": { "login": "bob" }
}

The action field describes what happened. For pull_request, possible values are opened, closed, edited, synchronize, labeled, unlabeled, assigned, unassigned, review_requested, review_request_removed, converted_to_draft, and ready_for_review.