Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use an aiohttp POST handler to receive a webhook, authenticate the sender before trusting the payload, parse the configured body format, and then handle or enqueue the event. For GitHub, verify the raw body against X-Hub-Signature-256 before parsing JSON; parsing a request is not authentication. The example below shows a small GitHub JSON receiver, plus what to change for URL-encoded deliveries and reliable production processing.

What the receiver needs to do

aiohttp is an asynchronous HTTP client/server framework for Python and asyncio. Its web server maps a route to an async handler that receives a web.Request and returns a response. A webhook is an HTTP request from a provider to that route; the route should not treat its public URL, event-name header, or payload fields as proof of sender identity.

The safe order is: read the original request bytes, validate the provider’s authentication mechanism, parse the body in the format configured with that provider, validate the event’s expected shape, and only then process or enqueue it. The exact signature headers, body formats, retry policy, and acknowledgement rules are provider-specific. The GitHub details below apply to GitHub deliveries, not to every webhook service.

Build a GitHub JSON endpoint

This example uses the Python standard library’s HMAC and constant-time comparison functions to check GitHub’s SHA-256 signature over the unchanged request body. Put the shared secret in the GITHUB_WEBHOOK_SECRET environment variable, configure GitHub to send JSON, and run the file with that variable set. The handler authenticates before decoding the JSON or using event metadata.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import hashlib
import hmac
import json
import os

from aiohttp import web

SECRET = os.environ["GITHUB_WEBHOOK_SECRET"].encode("utf-8")


def valid_github_signature(body: bytes, supplied: str | None) -> bool:
    if not supplied or not supplied.startswith("sha256="):
        return False
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, supplied)


async def receive_github(request: web.Request) -> web.Response:
    body = await request.read()
    signature = request.headers.get("X-Hub-Signature-256")
    if not valid_github_signature(body, signature):
        raise web.HTTPUnauthorized(text="Invalid webhook signature")

    try:
        event = json.loads(body)
    except (json.JSONDecodeError, UnicodeDecodeError):
        raise web.HTTPBadRequest(text="Expected valid JSON")

    delivery_id = request.headers.get("X-GitHub-Delivery")
    event_name = request.headers.get("X-GitHub-Event")
    if not delivery_id or not event_name:
        raise web.HTTPBadRequest(text="Missing GitHub delivery headers")

    # Replace this with validated dispatch or durable enqueueing.
    print(f"Received {event_name} delivery {delivery_id}")
    return web.json_response({"received": True})


app = web.Application(client_max_size=25 * 1024 * 1024)
app.add_routes([web.post("/webhooks/github", receive_github)])

if __name__ == "__main__":
    web.run_app(app)

The signature check expects GitHub’s documented X-Hub-Signature-256 value: the sha256= prefix followed by the hexadecimal SHA-256 HMAC of the body, keyed with the configured secret. The body must be the exact bytes received; do not parse and re-serialize JSON before computing the digest. GitHub recommends this header over the legacy X-Hub-Signature SHA-1 header. Keep the secret out of source control and logs.

The configured 25 MB request limit matches GitHub’s documented maximum payload size. aiohttp’s limit is an application safety setting, not a promise that all requests of that size can be processed cheaply. GitHub states that larger payloads are not delivered. Subscribe only to event types the application actually uses to avoid unnecessary deliveries.

Understand the handler, parsing, and response

Read bytes before decoding

await request.read() returns the raw body as bytes and aiohttp caches the result. That makes it suitable for signature verification, after which the example decodes those same bytes with json.loads. Alternatively, aiohttp provides await request.json(), which parses JSON and expects application/json by default. It also reads and caches the request body. Use it when no raw-body verifier is needed, or after you have authenticated the original body.

Use delivery headers as metadata

GitHub documents X-GitHub-Delivery as a globally unique delivery identifier and X-GitHub-Event as the event name. The event header can select a dispatch branch after authentication; it does not prove who sent the request. Check that the decoded JSON has the fields your chosen event requires rather than assuming every validly signed payload has the shape your application expects.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose a deliberate acknowledgement

The example returns a JSON response with the default success status when it has accepted the request. aiohttp lets a handler return a response with an explicit status and body, or raise an HTTP exception for an error. Whether to finish the business operation synchronously or enqueue it and acknowledge is a system-design choice: long work in the handler can make requests slow, while acknowledging before durable acceptance can lose work if the process stops. Check the selected provider’s current acknowledgement and retry policy; there is no single status code or timing rule established for all providers.

Support GitHub URL-encoded deliveries when configured

GitHub supports both application/json and application/x-www-form-urlencoded delivery formats. If you configure URL-encoded delivery, do not feed that body to a JSON parser. Parse the form using aiohttp’s form handling and extract the provider’s payload field according to the configured format. If you authenticate the delivery, verify the original bytes before interpreting the form, following the provider’s documented signature rules.

async def receive_form(request: web.Request) -> web.Response:
    body = await request.read()
    # Authenticate body here using the provider's documented scheme.
    form = await request.post()
    payload_text = form.get("payload")
    if not isinstance(payload_text, str):
        raise web.HTTPBadRequest(text="Missing payload form field")
    try:
        event = json.loads(payload_text)
    except json.JSONDecodeError:
        raise web.HTTPBadRequest(text="Invalid JSON in payload field")
    # Validate and dispatch event here.
    return web.json_response({"received": True})

This form parser illustrates the parsing distinction, not a replacement GitHub signature verifier. Keep authentication provider-specific and perform it before trusting the parsed event. aiohttp’s request.post() handles form-encoded and multipart POST parameters; it can raise HTTPRequestEntityTooLarge when the configured client_max_size is exceeded.

Make event processing safe to retry

A delivery can be received more than once in real integrations, so an application should decide what happens if the same event is processed again. GitHub’s delivery identifier gives you a useful key for recording and checking deliveries, but persistence and idempotency are application responsibilities. Store the identifier in a durable database with the processing state, and make updates idempotent where duplicate side effects would be harmful. Do not assume that an in-memory set protects against duplicates after a restart or across multiple server instances.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a handler that must do significant work, a common design is to validate and enqueue the authenticated event, then return success once the enqueue operation is durably accepted. A worker can perform slower processing independently. If enqueueing fails, choose a failure response consistent with the provider’s documented retry behavior. Do not return success merely because the HTTP handler ran if the event has not been safely recorded.

Run and expose the endpoint

  1. Install aiohttp: in an isolated Python environment, run python -m pip install aiohttp.
  2. Set the secret: export GITHUB_WEBHOOK_SECRET in the server environment using the same secret configured for the GitHub webhook. Do not hard-code a production secret in the file.
  3. Start the application: run python app.py. web.run_app starts the aiohttp server; for local development its default address is suitable for local testing, not direct public production exposure.
  4. Configure the provider: point the GitHub webhook at the publicly reachable HTTPS route ending in /webhooks/github, choose JSON if using the first handler, set the same secret, and select only the events your application handles.
  5. Deploy behind your normal web infrastructure: terminate HTTPS using an appropriately configured reverse proxy or hosting platform, and ensure it forwards the request body and headers without rewriting signed content. Restrict access to secrets and logs, and monitor handler failures and queue health.

The aiohttp stable web documentation page accessed for this guide labels itself 3.14.3; the request-reference documentation used for request semantics is on the project’s moving master branch. Confirm behavior against the aiohttp release you deploy. Provider documentation can change, so re-check GitHub’s current delivery guidance when configuring a live integration.

Common failures and fixes

Symptom Likely cause What to check
401 from the handler The signature is absent, malformed, computed from different bytes, or uses a different secret. Confirm the configured secret on both sides; verify the raw request body; use X-Hub-Signature-256 and compare the complete sha256=... value.
400 for a JSON request The body is malformed, the content type is not JSON when using request.json(), or the payload does not match the parser. Check the provider’s configured delivery format. Handle JSON and form-encoded requests with the appropriate parser instead of accepting arbitrary content.
413 or request-size exception The body exceeds the application’s configured aiohttp limit or an upstream proxy limit. Check both limits. For GitHub, remember its documented 25 MB maximum; increasing a local limit cannot make GitHub deliver a payload over its cap.
The wrong event branch runs The handler dispatches on unvalidated metadata or assumes an event’s payload schema. Authenticate first, then use X-GitHub-Event for routing and validate the event’s required fields.
The sender reports a failed delivery although work appears complete The handler returned an error, timed out, or lost connectivity before the provider received the response. Inspect server and proxy logs, use durable enqueueing for slow work, and consult that provider’s response and retry rules before changing acknowledgement behavior.
Events run twice The provider or network caused a duplicate delivery, or an earlier acknowledgement was not observed. Record the delivery identifier durably and make event effects idempotent; do not rely on an in-memory deduplication cache.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

This is a separate option for a webhook-driven workflow that also needs a screenshot of a public page; ScreenshotNeo is a screenshot API, not an aiohttp webhook receiver. One GET request captures a URL, and its response can be an image or PDF. The code below saves a WebP screenshot. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, with response headers identifying the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Learn about ScreenshotNeo, or sign up for 1,000 free screenshots a month with no card.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before you go live

  • Verify the provider’s signature against raw bytes before parsing or acting on the event.
  • Accept only the body formats configured for the endpoint and validate event-specific fields.
  • Set request-size limits that account for the provider’s cap and the limits of proxies in front of aiohttp.
  • Record delivery identifiers and make important side effects safe against duplicate processing.
  • Return success only when the event has been handled or durably accepted, following that provider’s acknowledgement rules.

Frequently Asked Questions

Does aiohttp automatically verify webhook signatures?

No. aiohttp provides request parsing and response mechanics; signature verification is application code or provider-specific library behavior.

Can the GitHub example receive URL-encoded payloads unchanged?

No. The first handler is for JSON. Use a form parser and the provider’s documented authentication rules when URL-encoded delivery is configured.

Which GitHub header identifies the event type?

After authenticating the request, use X-GitHub-Event as the event-name dispatch hint; it is not proof of sender identity.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.