Webhooks

Event-driven automation via HTTP callbacks. Receiving, sending, signature verification, and GitHub webhook patterns.

Webhook Concepts

Webhook vs polling — push beats pull
Polling:  Client asks server "anything new?" every N seconds
Webhook:  Server tells client "something happened" immediately

Polling wastes requests when nothing changes.
Webhooks deliver events in real time with zero wasted requests.

Receiving Webhooks

Minimal webhook receiver — Python + FastAPI
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib

app = FastAPI()
WEBHOOK_SECRET = "your-secret"  # from gopass/vault in production

@app.post("/webhook/github")
async def github_webhook(request: Request):
    body = await request.body()

    # Verify signature
    signature = request.headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), body, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = await request.json()
    event = request.headers.get("X-GitHub-Event")

    if event == "push":
        branch = payload.get("ref", "").split("/")[-1]
        print(f"Push to {branch} by {payload['pusher']['name']}")

    return {"status": "ok"}

Always verify webhook signatures. Without verification, anyone can POST fake events to your endpoint.

Minimal receiver — bash + netcat (testing only)
# Listen for a single webhook delivery (debugging)
while true; do
    echo -e "HTTP/1.1 200 OK\r\n\r\n" | nc -l -p 8080 | head -50
done

Sending Webhooks

Trigger a GitHub repository dispatch — spoke notifies hub
curl -X POST \
    -H "Accept: application/vnd.github+json" \
    -H "Authorization: Bearer $(gopass show -o github/pat)" \
    https://api.github.com/repos/EvanusModestus/domus-docs/dispatches \
    -d '{"event_type": "component-updated", "client_payload": {"repo": "domus-captures"}}'
Generic webhook delivery — notify on deploy completion
#!/bin/bash
set -euo pipefail

WEBHOOK_URL="https://hooks.example.com/deploy"

notify_webhook() {
    local status="$1"
    local message="$2"

    curl -sf -X POST "$WEBHOOK_URL" \
        -H "Content-Type: application/json" \
        -d "$(jq -n \
            --arg status "$status" \
            --arg message "$message" \
            --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
            '{status: $status, message: $message, timestamp: $timestamp}'
        )"
}

notify_webhook "success" "Deploy to production completed"

GitHub Webhooks

Configure a repository webhook
# Create webhook via gh CLI API
gh api repos/EvanusModestus/domus-docs/hooks \
    -f url="https://hooks.example.com/github" \
    -f content_type="json" \
    -f secret="$(gopass show -o webhooks/github-secret)" \
    -f events[]="push" \
    -f events[]="pull_request"
Common GitHub events
push              — commit pushed to a branch
pull_request      — PR opened, closed, merged, updated
issues            — issue created, edited, closed
release           — release published
workflow_run      — GitHub Actions workflow completed
repository_dispatch — custom event (API-triggered)

Webhook Security

HMAC signature verification — the standard pattern
import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

hmac.compare_digest is timing-attack resistant. Never use == for signature comparison.

Defense checklist
1. Verify HMAC signature on every request
2. Use HTTPS endpoint only
3. Store secret in gopass/vault, not in code
4. Validate payload schema before processing
5. Return 200 quickly, process asynchronously
6. Log all webhook deliveries for audit

Webhook Testing

Test with curl — simulate a webhook delivery
curl -X POST http://localhost:8000/webhook/github \
    -H "Content-Type: application/json" \
    -H "X-GitHub-Event: push" \
    -H "X-Hub-Signature-256: sha256=..." \
    -d '{"ref": "refs/heads/main", "pusher": {"name": "evan"}}'
ngrok — expose local server to the internet for testing
# Start local server
uvicorn app.main:app --port 8000

# In another terminal — create public URL
ngrok http 8000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhook/github
Replay failed deliveries — GitHub UI
Settings → Webhooks → Recent Deliveries
  → Click a delivery → "Redeliver" button

See Also

  • GitHub Actions — repository_dispatch for webhook-triggered workflows

  • API Webhooks — webhook patterns from the API perspective