Webhook and Health API
github-webhook-server exposes two primary HTTP endpoints under the same /webhook_server prefix:
POST /webhook_serverreceives GitHub webhook deliveries.GET /webhook_server/healthcheckprovides a lightweight liveness check.
The webhook endpoint is intentionally designed to return quickly, then continue the real work in the background. That keeps GitHub from timing out while the server does slower tasks such as GitHub API calls, repository cloning, PR checks, labels, comments, and other automation.
POST /webhook_server
Use POST /webhook_server as the webhook target in GitHub. The configured public URL must include the full path, not just the host:
webhook-ip: <HTTP://IP OR URL:PORT/webhook_server> # Full URL with path (e.g., https://your-domain.com/webhook_server or https://smee.io/your-channel)
Per-repository events in config.yaml control which GitHub events the server subscribes to when it creates or updates webhooks on startup:
events: # To listen to all events do not send events
- push
- pull_request
- pull_request_review
- pull_request_review_thread
- issue_comment
- check_run
- status
If you leave out events, the webhook registration logic falls back to all events. If you configure webhook-secret, the server also includes that same secret when it creates the webhook in GitHub.
What the endpoint validates before it accepts a delivery
A webhook only needs a small set of fields to be accepted at the HTTP layer:
X-GitHub-Eventmust be present.- The request body must be readable and valid JSON.
- The JSON must include
repository.nameandrepository.full_name. - If
webhook-secretis configured, the request must include a validX-Hub-Signature-256HMAC SHA256 signature. - If IP verification is enabled, the client IP must match the loaded GitHub and/or Cloudflare allowlist.
X-GitHub-Delivery is not required, but you should send it. The server echoes it back in the response and uses it for log correlation. If it is missing, the server uses unknown-delivery.
A successful request in the test suite uses the standard GitHub-style headers and gets back the queue acknowledgement response:
headers = {
"X-GitHub-Event": "pull_request",
"X-GitHub-Delivery": "test-delivery-123",
"x-hub-signature-256": signature,
"Content-Type": "application/json",
}
response = client.post("/webhook_server", content=payload_json, headers=headers)
assert response.status_code == 200
data = response.json()
assert data["status"] == 200
assert data["message"] == "Webhook queued for processing"
assert data["delivery_id"] == "test-delivery-123"
assert data["event_type"] == "pull_request"
Note: The server reads the raw body and parses JSON itself. In practice, the important part is that the body is valid JSON;
Content-Typeis not what determines acceptance.
Response codes
| Status | Meaning |
|---|---|
200 OK |
The request passed front-door validation and was queued for background processing. |
400 Bad Request |
Missing X-GitHub-Event, unreadable body, invalid JSON, missing repository, missing repository.name, missing repository.full_name, or invalid client IP metadata when IP filtering is active. |
403 Forbidden |
Missing or invalid X-Hub-Signature-256 when webhook-secret is enabled, or client IP is outside the configured allowlist. |
500 Internal Server Error |
The server could not load the configuration it needed to validate the request. |
Tests also confirm two practical edge cases:
- A bad signature returns
403withRequest signatures didn't match. - If no
webhook-secretis configured, the same endpoint still accepts a valid JSON delivery without signature verification.
Warning: If
webhook-secretis unset, the endpoint accepts unsigned webhook payloads. That may be fine on a private test setup, but it is not a safe default for an internet-exposed deployment.
What 200 OK really means
The most important thing to understand about POST /webhook_server is that 200 means “queued,” not “finished.”
Once the request passes validation, the server starts a background task and returns immediately:
# Start background task immediately using asyncio.create_task
# This ensures the HTTP response is sent immediately without waiting
# Store task reference for observability and graceful shutdown
task = asyncio.create_task(
process_with_error_handling(
_hook_data=hook_data,
_headers=request.headers,
_delivery_id=delivery_id,
_event_type=event_type,
)
)
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
# Return 200 immediately with JSONResponse for fastest serialization
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
"status": status.HTTP_200_OK,
"message": "Webhook queued for processing",
"delivery_id": delivery_id,
"event_type": event_type,
},
)
This design exists to avoid GitHub webhook timeouts. The server only does just enough validation to know the delivery can be processed. The slower work happens after the response has already been sent.
In practice, that means:
200 OKmeans the request was valid enough to enter the processing pipeline.200 OKdoes not mean the automation finished successfully.- Repository lookup failures, GitHub API failures, and handler errors can still happen later.
- Those later failures are logged, but they do not change the already-sent HTTP response.
Tip: Treat the returned
delivery_idas your main breadcrumb. It is the quickest way to match a GitHub delivery to the server’s background-processing logs.
What happens after the delivery is queued
After the webhook is queued, the background processor routes the event. The code handles GitHub events including ping, push, pull_request, pull_request_review, pull_request_review_thread, issue_comment, check_run, and status.
Not every accepted delivery turns into visible automation. Some are intentionally accepted and then skipped in the background, for example:
- a
pingevent - a
statusevent that is stillpending - a
check_runthat is not yetcompleted - a push deletion event
- an event that does not resolve to an open pull request
That is why the HTTP response should be treated as an acknowledgement, not a final execution result.
Tracing a delivery
Structured webhook logs are written as daily JSONL files under the server data directory in logs/webhooks_YYYY-MM-DD.json. With the default data directory, that is /home/podman/data/logs/. Deployments can override the data directory with WEBHOOK_SERVER_DATA_DIR.
The delivery_id returned by the endpoint corresponds to the same delivery identifier used in the logs as hook_id, so it is the best way to trace what happened after the request was queued.
IP allowlist behavior
If you enable verify-github-ips and/or verify-cloudflare-ips, the endpoint enforces source-IP filtering before it even parses the webhook body. When those settings are off, the webhook endpoint does not perform source-IP checks.
That hardening also affects startup: the application loads the allowlists during startup, and if IP verification is enabled but no valid networks can be loaded, the server fails closed rather than starting in an insecure state.
Warning: With IP verification enabled, a startup failure to load valid GitHub and/or Cloudflare ranges means the API will not come up at all. That is intentional fail-closed behavior.
GET /webhook_server/healthcheck
GET /webhook_server/healthcheck is a simple liveness endpoint. If the application is up and serving requests, it returns:
@FASTAPI_APP.get(f"{APP_URL_ROOT_PATH}/healthcheck", operation_id="healthcheck")
def healthcheck() -> dict[str, Any]:
return {"status": requests.codes.ok, "message": "Alive"}
This endpoint is intentionally lightweight. It does not do a live GitHub API call, repository lookup, or end-to-end webhook test on every request. It simply answers whether the application process is up and serving HTTP.
That makes it a good fit for container and load-balancer health checks. The project’s Dockerfile uses it exactly that way:
HEALTHCHECK CMD curl --fail http://127.0.0.1:5000/webhook_server/healthcheck || exit 1
ENTRYPOINT ["tini", "--", "uv", "run", "entrypoint.py"]
Tip: Use
/webhook_server/healthcheckto answer “is the server alive?” Use a real GitHub delivery plus the webhook logs to answer “is my automation working end to end?”
Troubleshooting checklist
If GitHub shows 200 OK but nothing happened, check these first:
- The configured
webhook-ipincludes the exact/webhook_serverpath. - The repository exists in
config.yaml. - The repository’s
eventslist includes the event GitHub actually sent. webhook-secretmatches GitHub’s configured secret, if you use one.- The request source IP is allowed, if IP verification is enabled.
- The returned
delivery_idappears in that day’s structured log file.
The core rule for this API is simple: POST /webhook_server returns fast so GitHub can move on. The real answer about success or failure lives in the background processing logs, not in the HTTP 200 alone.