webhook-receiver

Featured

Set up a webhook endpoint that receives and processes incoming webhooks with signature verification.

Category: Backend Tier: Highly useful within a category Source: Newly authored Updated: 2026-07-20

What it does

Set up a webhook endpoint that receives and processes incoming webhooks with signature verification.

How an agent uses it

Use this skill when:

  • An external provider (payments, git host, CI, SaaS) needs to **push** events to

you via an HTTPS callback instead of you polling their API.

  • You need to react to events in (near) real time: `payment.succeeded`,

`push.created`, `issue.opened`, `order.fulfilled`, etc.

  • You want a single, auditable receive → verify → process → respond pipeline.
  • The integration must be replay-safe (a retried delivery must not double-fire a

side effect).

Do **not** use this skill for: outbound API calls (that's a normal HTTP client),

long-polling/streaming consumers (use the provider's SDK), or receiving files

larger than a few MB (use signed upload URLs instead).

What you get

Install this skill and your Hermes agent can set up a webhook endpoint that receives and processes incoming webhooks with signature verification. No manual setup, no scripts to run — the agent handles it.

Install command

hermes skills install https://raw.githubusercontent.com/THEROCKSSS/hermes-skills-portfolio/main/skills/webhook-receiver/SKILL.md
View SKILL.md on GitHub
---

name: webhook-receiver

description: Use when the user needs an HTTP endpoint that receives inbound webhooks from an external service (Stripe, GitHub, Slack, Shopify, CI) and must verify the sender's signature, parse the payload safely, dedupe retried deliveries, and return the right status code — not for outbound API calls or long-polling/streaming consumers.

version: 1.0.0

author: Hermes Agent

license: MIT

metadata:

  hermes:

    tags: [webhooks, hmac-verification, idempotency, http-api, event-ingestion]

    related_skills: [http-api-tester, env-config-manager, telegram-bot-build]

---



# webhook-receiver



## Overview



Turn any Hermes-managed host into a trustworthy webhook ingestion point. This

skill gives you a production-ready pattern for receiving HTTP POSTs from external

services, verifying they really came from the sender, parsing the payload safely,

and returning the status code the sender expects — without dropping events under

load or leaking secrets in logs.



## When to Use



Use this skill when:



- An external provider (payments, git host, CI, SaaS) needs to **push** events to

  you via an HTTPS callback instead of you polling their API.

- You need to react to events in (near) real time: `payment.succeeded`,

  `push.created`, `issue.opened`, `order.fulfilled`, etc.

- You want a single, auditable receive → verify → process → respond pipeline.

- The integration must be replay-safe (a retried delivery must not double-fire a

  side effect).



Do **not** use this skill for: outbound API calls (that's a normal HTTP client),

long-polling/streaming consumers (use the provider's SDK), or receiving files

larger than a few MB (use signed upload URLs instead).



## Architecture



```

   External Sender                Your Endpoint                    Your System

┌──────────────┐   HTTPS POST   ┌──────────────────┐   enqueue/    ┌──────────────┐

│ Stripe/GitHub│ ─────────────▶ │  /webhooks/<src> │ ───────────▶ │  handler /

│  (signs body)│  body + HMAC   │  1. verify sig   │   async job   │  DB / action │

└──────────────┘  header        │  2. parse JSON    │               └──────────────┘

                               │  3. idempotent   │

                               │  4. respond 2xx  │

                               └──────────────────┘

```



Core principles:



- **Verify before you trust.** Reject any request whose signature does not match

  before parsing or acting on the body.

- **Return fast.** Do the minimum in the request thread (verify + enqueue), then

  process asynchronously. A slow handler makes the sender retry and floods you.

- **Be idempotent.** Senders often retry. Use the event ID + a dedup store so a

  redelivered event runs its side effect at most once.

- **Separate routes per source.** `/webhooks/stripe`, `/webhooks/github` — each

  source has its own secret and verification scheme.



## Workflow



### 1. Create the endpoint



Expose a single POST route per source behind TLS. Keep it thin:



```python

from flask import Flask, request, abort

import hmac, hashlib, json



app = Flask(__name__)



@app.post("/webhooks/stripe")

def stripe_webhook():

    payload = request.get_data()          # raw bytes — needed for HMAC

    sig = request.headers.get("Stripe-Signature", "")

    if not verify_stripe(payload, sig):

        abort(400)

    event = json.loads(payload)

    handle_event(event)                   # enqueue, don't block

    return "", 200

```



### 2. Validate signatures



Never act on an unverified body. Compute HMAC-SHA256 over the **raw** bytes with

your shared secret and compare against the sender's header using a constant-time

compare. See [Signature Verification](#signature-verification).



### 3. Process the payload



- Parse defensively: wrap `json.loads` in try/except; reject non-JSON with 400.

- Extract the event type and a stable event ID.

- Check the dedup store; if already processed, return 200 immediately.

- Otherwise enqueue for async processing and record the event ID.



```python

def handle_event(event):

    event_id = event.get("id")

    if event_id and seen(event_id):

        return

    queue.put(event)                      # e.g. Redis/RQ, SQS, a worker

    mark_seen(event_id)

```



### 4. Respond



Send the smallest correct response:



- **200 / 204** — accepted. Sender stops retrying.

- **400** — malformed or bad signature. (Most senders will not retry 4xx.)

- **500** — only on genuine internal failure; senders will retry.

- Keep the body empty or a tiny JSON ack. Do not echo the payload back.

- Target a p95 response under ~1s; offload heavy work to the worker.



## Signature Verification



Senders use HMAC. The exact header/algorithm varies by provider:



| Provider  | Header                | Scheme                                              |

|-----------|-----------------------|----------------------------------------------------|

| Stripe    | `Stripe-Signature`    | `t=<ts>,v1=<HMAC-SHA256(raw, secret)>`             |

| GitHub    | `X-Hub-Signature-256` | `sha256=<HMAC-SHA256(raw, secret)>`                |

| Slack     | `X-Slack-Signature`   | `v0=<HMAC-SHA256("v0:"+ts+":"+raw, secret)>`       |



Generic verifier:



```python

def constant_time_equal(a, b):

    return hmac.compare_digest(a, b)



def verify(payload: bytes, header_sig: str, secret: str, prefix: str = "sha256="):

    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()

    provided = header_sig[len(prefix):] if header_sig.startswith(prefix) else header_sig

    return constant_time_equal(expected, provided)

```



Rules:

- Use `hmac.compare_digest`, never `==` (avoids timing leaks).

- Verify over **raw bytes**, not the re-serialized JSON (whitespace/key order

  changes the digest).

- For timestamped schemes (Stripe, Slack), reject if `|now - ts| > 300s` to

  prevent replay attacks.

- Store the secret in an env var / secret manager. Never in source control.



## Payload Handling



- **Define a schema.** Map known event types to handlers; ignore unknown types

  with 200 (senders add new types without warning).

- **Validate shape, not just presence.** Use a lightweight validator (pydantic,

  jsonschema) so a malformed event fails loudly rather than crashing mid-flight.

- **Don't trust types.** A `price` field could arrive as string; coerce

  explicitly.

- **Log minimally.** Record `event_id`, `type`, and `source` — never the full

  body or headers (they may contain PII or secrets).

- **Backpressure.** If the queue is full, return 503 so the sender retries later

  instead of losing the event.



## Error Handling



- **Bad signature / malformed JSON** → 400, no retry expected. Log as security

  event.

- **Unknown event type** → 200, drop silently (or to a dead-letter log).

- **Transient processing failure** → raise so the worker retries with backoff.

  Keep the HTTP layer returning 200 once the event is durably enqueued.

- **Duplicate delivery** → detected via dedup store, return 200, no side effect.

- **Dead letters.** After N retries, move to a DLQ for human inspection rather

  than dropping.

- Never let an unhandled exception bubble into a 500 for a *verified* event that

  you already accepted — that triggers wasteful redelivery.



## Common Pitfalls



1. **Verifying the wrong bytes.** Re-serializing JSON before computing the HMAC is the #1 bug —

   whitespace/key-order changes the digest. Always digest the raw body (`request.get_data()`),

   never `json.dumps(parsed_body)`.

2. **Using `==` for signature comparison.** A naive string compare leaks timing information.

   Always use `hmac.compare_digest`.

3. **Doing the real work inside the request handler.** Slow processing causes the sender to time

   out and retry, which compounds load. Verify + enqueue, then return — process asynchronously.

4. **Skipping the dedup/idempotency check.** Senders retry on any ambiguous response. Without a

   dedup store keyed on event ID, a retried delivery re-runs the side effect (double-charge,

   double-post).

5. **Leaving the shared secret in git history or logs.** Rotate immediately if leaked; store it

   in a secret manager, and never log the full request body or headers.

6. **Skipping the replay-window check on timestamped schemes.** Stripe and Slack sign with a

   timestamp; without enforcing `|now - ts| > 300s` rejection, a captured request can be replayed

   later even with a valid signature.

7. **Returning 500 for an unknown-but-harmless event type.** That triggers sender retries for

   something that was never going to be handled. Return 200 and drop (or log to a dead-letter

   sink) instead.

8. **Sharing one route across sources instead of one per source.** A single shared endpoint means

   one compromised secret affects every integration, and there's no way to rotate one source's

   secret without touching the others.

9. **Not capping request body size.** An unbounded `Content-Length` read on `/webhooks/*` is a

   trivial DoS vector — cap it (e.g. 1–5 MB) before reading the body.



## Verification Checklist



- [ ] Endpoint is HTTPS-only and returns 200 for a valid signed test event.

- [ ] Invalid signature returns 400 and is logged as a security event.

- [ ] Same event ID delivered twice runs the side effect once.

- [ ] Response time < 1s even when processing is slow (offloaded to worker).

- [ ] Secret is in a secret manager, absent from source and logs.

- [ ] Replay with a stale timestamp is rejected.

# webhook-receiver



A public Hermes skill for building **secure, reliable inbound webhook endpoints**.



Receive events from Stripe, GitHub, Slack, Shopify, CI systems, and any other

provider that pushes via HTTPS — verify they really came from the sender, process

them safely, and respond with the exact status code the sender expects.



> Agent + skill = a working webhook endpoint, not a tutorial.



## Why this exists



Most webhook integrations break in the same three places: the signature check is

wrong, retries cause double-processing, or a slow handler triggers a retry

storm. `webhook-receiver` encodes the verify → parse → dedupe → enqueue →

respond pipeline that fixes all three by default.



## What you get



- **Thin, TLS-only endpoints** — one POST route per source, returning in <1s.

- **HMAC signature verification** — constant-time compare over raw bytes, with

  per-provider header schemes (Stripe, GitHub, Slack) and replay-window checks.

- **Idempotent processing** — dedup on event ID so retried deliveries never

  double-fire a side effect.

- **Safe payload handling** — schema validation, minimal logging (no PII/secrets

  in logs), and backpressure via 503.

- **Clear error contract** — 200 accept, 400 reject, 500 retry; unknown event

  types dropped cleanly.



## Install



Point Hermes at the skill manifest:



```

https://github.com/THEROCKSSS/hermes-skills-portfolio/blob/main/skills/webhook-receiver/SKILL.md

```



Or clone the portfolio and load `skills/webhook-receiver` locally.



## Quick start



```python

import hmac, hashlib, json

from flask import Flask, request, abort



app = Flask(__name__)

SECRET = "your-secret-from-secret-manager"   # never hardcode



@app.post("/webhooks/stripe")

def stripe_webhook():

    payload = request.get_data()             # raw bytes for HMAC

    sig = request.headers.get("Stripe-Signature", "")

    expected = hmac.new(SECRET.encode(), payload, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, sig.split("v1=")[-1]):

        abort(400)                           # verify before trust

    event = json.loads(payload)

    handle_event(event)                      # enqueue, don't block

    return "", 200

```



See `SKILL.md` for the full workflow, signature tables, dedup patterns, and the

verification checklist.



## Author



Part of the public [Hermes Skills Portfolio](https://github.com/THEROCKSSS/hermes-skills-portfolio)

by **Alex**.