Computing Foundations › APIs and the Web › Day 26
Hands-on lab — Day 26: Webhooks and Event-Driven APIs
- ← Back to the Day 26 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-026-webhooks-and-event-driven-apis/
Commands
Setup
cd labs/sections/computing-foundations/day-026-webhooks-and-event-driven-apis Run
bash examples/webhook_demo.sh
bash starter/webhook_demo.sh Test
bash tests/run_tests.sh File tree
examples/webhook_demo.sh expected-output/sample-run.txt metadata.yml README.md requirements/README.md security.md starter/webhook_demo.sh starter/webhook-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 026 lab — Simulate a Webhook Delivery
Lesson
- Lesson title: Webhooks and Event-Driven APIs
- Day number: 26 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-026-webhooks-and-event-driven-apis
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-026-webhooks-and-event-driven-apiswhen the site is running.
Purpose
Day 26's lesson explained the shift from pull to push — how a server calls your URL when an event happens. This lab makes it concrete without needing a public server: you play both sides of a webhook on your own machine. You compute the HMAC signature a sender attaches, deliver a sample payload to a free echo service so you can see exactly what a receiver receives, verify a genuine signature and reject a forged one, and watch a retry loop back off on a simulated failure.
Learning objectives
- Compute an HMAC-SHA-256 signature over a raw payload with a shared secret.
- Deliver a JSON payload as an HTTP
POSTand read what the receiver sees. - Verify a delivery by recomputing and comparing the signature, and detect a tampered payload.
- Explain, from a working retry loop, why at-least-once delivery requires idempotent handling.
Prerequisites
- The Day 26 lesson, and days 18–25 (HTTP, HTTPS/TLS, JSON, API authentication).
- A terminal with
curlandopenssl(both preinstalled). Network access for the live delivery step only.
Supported operating systems
- macOS and Linux — fully supported (
curlandopensslpreinstalled). - Windows — use WSL, or run the individual
curl/opensslcommands in a shell that provides them.
Hardware requirements
Any computer with a terminal. The live delivery step needs an internet connection; every other step is local.
Required software
curl and openssl only — preinstalled almost everywhere. See
requirements/README.md.
Free and open-source options
Everything here is free: curl and openssl are open source, and the echo
service (httpbin.org) is a free public endpoint. No accounts or API keys.
Installation
None. Change into this directory:
cd labs/sections/computing-foundations/day-026-webhooks-and-event-driven-apis
File structure
day-026-webhooks-and-event-driven-apis/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── webhook_demo.sh ← YOUR working file (4 exercises)
│ └── webhook-worksheet.md ← record your signature, echo, and notes
├── examples/
│ └── webhook_demo.sh ← completed reference implementation
├── tests/
│ └── run_tests.sh ← structure + local HMAC + (online) echo checks
├── expected-output/
│ └── sample-run.txt ← real captured run (online)
├── requirements/README.md
├── troubleshooting.md
└── security.md
How to run
## 1. See the finished flow first (delivery step needs network)
bash examples/webhook_demo.sh
## 2. Complete the four exercises in the starter, then run it
bash starter/webhook_demo.sh
## 3. Check your work
bash tests/run_tests.sh
What the commands do
bash examples/webhook_demo.sh— runs the full simulation in three sections: (1) computes the HMAC signature a sender attaches; (2)POSTs the sample payload tohttpbin.org/postand prints the body and signature header the receiver echoes back; (3) verifies a genuine signature (MATCH), rejects a tampered one (MISMATCH), and runs a retry loop that stops on a200. It degrades gracefully offline.bash starter/webhook_demo.sh— the same skeleton with four numbered exercises; each names the exactopenssl/curlcommand to use.bash tests/run_tests.sh— always runs structure and local HMAC checks (both scripts parse, the example exercises each step, the signature is a 64-hex string equal to the known value, a tampered body differs). When online it also confirms the echo endpoint returns your payload and signature; offline (or when httpbin is transiently down) that one check is skipped, never failed. Exits 0 on success.
Expected output
See expected-output/sample-run.txt for a
real captured run. The signature is deterministic
(sha256=cadd3bf8...e3c0); your trace ID and the exact echo formatting will
differ.
Validation steps
bash examples/webhook_demo.shprints all three sections without errors.- The signature is 64 hexadecimal characters and equals
cadd3bf8973d72aec386c06d46c8803f68aa1f6936e9ee4a334adedace74e3c0. - The echo section shows your exact payload body and
X-Webhook-Signature. - The verify section shows MATCH for the genuine payload and MISMATCH for the
tampered one, and the retry loop stops on the
200. - The tests pass.
Tests
bash tests/run_tests.sh
Expected final line online: 15 checks, 0 failure(s), 0 skip(s). Offline the
live delivery check is skipped: 14 checks, 0 failure(s), 1 skip(s). Either
way the command exits 0 on success, so it can run in CI.
Cleanup
Nothing to clean up — the scripts write no files and make one ordinary web
request. Reset your edited starter with git checkout -- starter/webhook_demo.sh.
Troubleshooting
See troubleshooting.md — signature mismatches (trailing
newline), openssl formatting, httpbin transient failures, the skip on the
network check, and offline behavior.
Security notes
See security.md. Short version: the demo secret is fake — never use a real one in code; verifying signatures is what stops forged webhooks; always use HTTPS for a real receiver; never send real secrets to an echo service.
Extension exercises
- Add idempotency: record each delivery's ID and make a repeated ID
acknowledge with
200but act only once; send the same delivery twice and prove the action happened once. - Add a timestamp inside the signed payload and reject deliveries older than five minutes, defending against replay of a captured message.
- Send a tampered payload with the original signature header to the echo service, recompute on receipt, and confirm your verify step would reject it.
Navigation
- Previous day: Day 25 — API Authentication: Keys, Tokens, and OAuth.
- Next day: Day 27 — Rate Limits, Pagination, and Error Handling.
Expected output
sample-run.txt
Day 026 — Simulate a Webhook Delivery
Playing both sender and receiver locally; echo endpoint: https://httpbin.org/post
=== 1. Compute the webhook signature (what the SENDER does) ===
payload: {"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}
signature: sha256=cadd3bf8973d72aec386c06d46c8803f68aa1f6936e9ee4a334adedace74e3c0
(64 hex characters)
=== 2. Deliver the payload to an echo endpoint (what the RECEIVER sees) ===
the receiver received this body:
{"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}
and this signature header:
sha256=cadd3bf8973d72aec386c06d46c8803f68aa1f6936e9ee4a334adedace74e3c0
=== 3. Verify + retry ===
receiver recomputes signature over the body it got... MATCH -> would reply 200 OK
tampered payload recomputes to a different signature... MISMATCH -> reject (forged)
attempt 1: server returned 503 -> not 2xx, retrying after backoff...
attempt 2: server returned 503 -> not 2xx, retrying after backoff...
attempt 3: server returned 200 -> acknowledged, stop retrying
Done. You signed a payload, saw what a receiver receives, verified a
genuine signature, rejected a forged one, and watched retries back off.
Source files
examples/webhook_demo.sh (6226 bytes)
#!/usr/bin/env bash
# Day 026 lab — Simulate a Webhook Delivery (completed reference implementation).
#
# A real webhook needs a public server that another company's system can reach.
# You do not have one, so this script plays BOTH roles on your own machine:
# 1. SENDER — compute the HMAC signature for a sample event payload.
# 2. RECEIVER — POST the payload to a free echo service (httpbin.org) so you
# can see exactly what a webhook receiver would receive, then recompute
# and compare the signature (accept a genuine one, reject a forged one).
# 3. RELIABILITY — run a retry loop that re-POSTs on a simulated non-2xx
# status and stops once it gets a 2xx acknowledgement.
#
# Run from the lab directory: bash examples/webhook_demo.sh
#
# The signature and verify steps are LOCAL and need no network. The delivery
# step reaches https://httpbin.org; offline, it degrades with a clear message
# and the script still exits 0.
set -u
# The sample event. printf '%s' (below) prints it with NO trailing newline, so
# the bytes we SIGN are exactly the bytes we SEND — this matters for HMAC.
PAYLOAD='{"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}'
# A FAKE secret, for the demo only. A real secret is a credential: keep it out
# of source code and logs (see security.md).
SECRET='whsec_demo_do_not_use_in_production'
HTTPBIN='https://httpbin.org'
rule() { printf '\n=== %s ===\n' "$1"; }
# Compute HMAC-SHA-256 of stdin with the given key; print just the 64-hex digest.
hmac_sha256() {
local key="$1"
openssl dgst -sha256 -hmac "$key" | sed 's/^.*= //'
}
# Return 0 if httpbin appears reachable, else 1.
have_network() {
curl -s -o /dev/null --max-time 12 "${HTTPBIN}/get" 2>/dev/null
}
# ---------------------------------------------------------------------------
# 1. What the SENDER does: sign the raw payload.
# ---------------------------------------------------------------------------
show_sign() {
rule "1. Compute the webhook signature (what the SENDER does)"
local sig
sig="$(printf '%s' "${PAYLOAD}" | hmac_sha256 "${SECRET}")"
echo "payload: ${PAYLOAD}"
echo "signature: sha256=${sig}"
echo "(${#sig} hex characters)"
}
# ---------------------------------------------------------------------------
# 2. What the RECEIVER sees: deliver to an echo endpoint.
# ---------------------------------------------------------------------------
show_deliver() {
rule "2. Deliver the payload to an echo endpoint (what the RECEIVER sees)"
local sig response
sig="$(printf '%s' "${PAYLOAD}" | hmac_sha256 "${SECRET}")"
if ! have_network; then
echo "OFFLINE: could not reach ${HTTPBIN}. Skipping the live delivery."
echo "(The signature and verify steps above and below need no network.)"
return
fi
# A real webhook is exactly this: a POST with a JSON body, a Content-Type,
# an event-type header, and a signature header. httpbin echoes it all back,
# so its response shows what a receiver would parse. -f fails on HTTP errors;
# --retry with --retry-all-errors rides past httpbin's frequent transient
# gateway hiccups and transfer resets (a real sender retries too).
response="$(curl -sf --max-time 25 --retry 6 --retry-delay 2 --retry-all-errors \
-X POST "${HTTPBIN}/post" \
-H 'Content-Type: application/json' \
-H 'X-Webhook-Event: invoice.paid' \
-H "X-Webhook-Signature: sha256=${sig}" \
-d "${PAYLOAD}" 2>/dev/null)" || response=""
if [ -z "${response}" ]; then
echo "(could not reach ${HTTPBIN} — transient error; try again in a moment)"
return
fi
# Pull the echoed body and signature header back out of httpbin's JSON with
# grep/sed only (no jq dependency).
echo "the receiver received this body:"
printf '%s\n' "${response}" | sed -n 's/.*"data": "\(.*\)", *$/\1/p' | sed 's/\\"/"/g' | head -n 1
echo "and this signature header:"
printf '%s\n' "${response}" | sed -n 's/.*"X-Webhook-Signature": "\(sha256=[0-9a-f]*\)".*/\1/p' | head -n 1
}
# ---------------------------------------------------------------------------
# 3. Verify (recompute + compare) and retry on non-2xx.
# ---------------------------------------------------------------------------
show_verify_and_retry() {
rule "3. Verify + retry"
local sent_sig recomputed tampered_payload tampered_sig
sent_sig="$(printf '%s' "${PAYLOAD}" | hmac_sha256 "${SECRET}")"
# The RECEIVER recomputes the signature over the body it got and compares.
recomputed="$(printf '%s' "${PAYLOAD}" | hmac_sha256 "${SECRET}")"
if [ "${sent_sig}" = "${recomputed}" ]; then
echo "receiver recomputes signature over the body it got... MATCH -> would reply 200 OK"
else
echo "receiver recomputes signature over the body it got... MISMATCH -> reject"
fi
# A forged/tampered body recomputes to a DIFFERENT signature -> rejected.
tampered_payload='{"event":"invoice.paid","id":"evt_28a7","data":{"amount":999999,"currency":"usd"}}'
tampered_sig="$(printf '%s' "${tampered_payload}" | hmac_sha256 "${SECRET}")"
if [ "${sent_sig}" = "${tampered_sig}" ]; then
echo "tampered payload recomputes to the SAME signature... (should not happen)"
else
echo "tampered payload recomputes to a different signature... MISMATCH -> reject (forged)"
fi
# A sender retries until it gets a 2xx. We SIMULATE a receiver that fails
# twice (503) then succeeds (200), to show at-least-once retrying + backoff.
local -a statuses=(503 503 200)
local attempt=1 code
for code in "${statuses[@]}"; do
if [ "${code}" -ge 200 ] && [ "${code}" -lt 300 ]; then
echo "attempt ${attempt}: server returned ${code} -> acknowledged, stop retrying"
break
else
echo "attempt ${attempt}: server returned ${code} -> not 2xx, retrying after backoff..."
fi
attempt=$((attempt + 1))
done
}
main() {
echo "Day 026 — Simulate a Webhook Delivery"
echo "Playing both sender and receiver locally; echo endpoint: ${HTTPBIN}/post"
show_sign
show_deliver
show_verify_and_retry
echo
echo "Done. You signed a payload, saw what a receiver receives, verified a"
echo "genuine signature, rejected a forged one, and watched retries back off."
}
main "$@"
metadata.yml (593 bytes)
lesson_id: D026
day: 26
kind: api-example
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-026-webhooks-and-event-driven-apis
run_commands:
- bash examples/webhook_demo.sh
- bash starter/webhook_demo.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/webhook_demo.sh # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (curl 8.7.1, OpenSSL 3.6.3), online, bash tests/run_tests.sh → 15 checks, 0 failure(s), 0 skip(s)'
requirements/README.md (881 bytes)
# Dependencies — Day 026 lab
**`curl` and `openssl` only** — both preinstalled on macOS and mainstream
Linux (and available on Windows 10+ / WSL). No package installs, no accounts,
no API keys.
- `curl` — makes the sample webhook delivery (a `POST`) to a public echo
service so you can see what a receiver receives.
- `openssl` — computes the HMAC-SHA-256 signature that authenticates a
webhook, exactly as a real sender and receiver would.
The lab reaches one free public test service — `https://httpbin.org` — for the
live delivery step, so that step needs network access. Everything else (the
signature computation, the verify-and-compare, the retry simulation) is
**local** and works offline. With no network the scripts print a clear message
and the tests **skip** (never fail) the live delivery check, still verifying
the HMAC locally. No `sudo` required.
starter/webhook_demo.sh (3763 bytes)
#!/usr/bin/env bash
# Day 026 starter — Simulate a Webhook Delivery.
#
# Complete the FOUR exercises below. Each names the exact command to use.
# You will play both sides of a webhook: the SENDER that signs and delivers a
# payload, and the RECEIVER that verifies it. The completed reference is in
# ../examples/webhook_demo.sh — try to build it yourself first.
#
# Run from the lab directory: bash starter/webhook_demo.sh
set -u
# The sample event. Use printf '%s' (NOT echo) so no trailing newline is added
# and the bytes you SIGN are exactly the bytes you SEND.
PAYLOAD='{"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}'
# A FAKE secret, for the demo only. Never hard-code a real secret (see security.md).
SECRET='whsec_demo_do_not_use_in_production'
HTTPBIN='https://httpbin.org'
rule() { printf '\n=== %s ===\n' "$1"; }
echo "Day 026 starter — Simulate a Webhook Delivery"
# ---------------------------------------------------------------------------
# Exercise 1: compute the signature (what the SENDER does).
# Replace the echo with:
# printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET"
# It should print a 64-hex-character digest.
# ---------------------------------------------------------------------------
rule "1. Compute the signature"
echo "EXERCISE — your turn: HMAC-SHA-256 the PAYLOAD with the SECRET"
# ---------------------------------------------------------------------------
# Exercise 2: deliver the payload to the echo endpoint (what the RECEIVER sees).
# Replace the echo with a POST that carries the body and a signature header:
# sig=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')
# curl -sf --max-time 25 --retry 6 --retry-delay 2 --retry-all-errors \
# -X POST "$HTTPBIN/post" \
# -H 'Content-Type: application/json' \
# -H 'X-Webhook-Event: invoice.paid' \
# -H "X-Webhook-Signature: sha256=$sig" \
# -d "$PAYLOAD"
# httpbin echoes your body and headers back — that echo is what a receiver parses.
# ---------------------------------------------------------------------------
rule "2. Deliver to the echo endpoint"
echo "EXERCISE — your turn: POST the PAYLOAD to $HTTPBIN/post and read the echo"
# ---------------------------------------------------------------------------
# Exercise 3: verify by recomputing and comparing (what the RECEIVER does).
# Replace the echo with:
# sent=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')
# recomputed=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')
# [ "$sent" = "$recomputed" ] && echo "MATCH -> accept" || echo "MISMATCH -> reject"
# Then repeat with a TAMPERED payload (change 4200 to 999999) and confirm it MISMATCHES.
# ---------------------------------------------------------------------------
rule "3. Verify the signature"
echo "EXERCISE — your turn: recompute the signature and compare; then try a tampered body"
# ---------------------------------------------------------------------------
# Exercise 4: simulate a retry loop (what the SENDER does on a non-2xx).
# Replace the echo with a loop over simulated statuses that stops on a 2xx:
# for code in 503 503 200; do
# if [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; then
# echo "returned $code -> acknowledged, stop retrying"; break
# else
# echo "returned $code -> not 2xx, retrying after backoff..."
# fi
# done
# ---------------------------------------------------------------------------
rule "4. Retry on non-2xx"
echo "EXERCISE — your turn: loop over 503 503 200 and stop retrying on the 200"
echo
echo "When all four are done, compare your output with ../examples/webhook_demo.sh"
starter/webhook-worksheet.md (1390 bytes)
# Webhook worksheet — Day 026
Run the commands (from the examples or your completed starter) and record what
YOUR run produced. The signature is deterministic, so it will match everyone
else's for the same payload and secret — but the echo response and trace ID
are unique to your run.
Sample event used:
`{"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}`
Secret used: `whsec_demo_do_not_use_in_production`
## 1. The signature your payload + secret produces
- Command: `printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET"`
- Signature (paste the full value): `sha256=____`
- How many characters is the hex digest? `____` (should be 64)
## 2. What the echo endpoint reported back
Paste the body the receiver received:
```
____
```
Paste at least one header the receiver saw (e.g. `X-Webhook-Event` or
`X-Webhook-Signature`):
```
____
```
## 3. One reason webhooks retry
- In your own words, one concrete reason a webhook sender retries a delivery:
> _your answer here_
- The problem retrying creates for a receiver, and the property that solves it
(name it):
> _your answer here_
## 4. Tampering detection
Change one character of the payload (for example `4200` → `999999`), re-run the
signature step, and answer:
- How much of the signature changed? `____`
- Why does that make tampering detectable?
> _your answer here_
tests/run_tests.sh (4511 bytes)
#!/usr/bin/env bash
# Tests for the Day 026 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Two kinds of checks:
# * Structure + LOCAL checks always run and must pass: files present, both
# scripts parse, the example exercises each core step, the starter names
# its four exercises, and — with NO network — the HMAC signature computes
# to the known 64-hex value (this is pure openssl, no internet).
# * The NETWORK check runs only when httpbin.org is reachable: it POSTs the
# payload and confirms the echo contains it. Offline, or when httpbin is
# transiently down, that check is SKIPPED (never failed).
#
# The script exits 0 when no non-skipped check failed, online or offline.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTPBIN='https://httpbin.org'
PAYLOAD='{"event":"invoice.paid","id":"evt_28a7","data":{"amount":4200,"currency":"usd"}}'
SECRET='whsec_demo_do_not_use_in_production'
EXPECTED_SIG='cadd3bf8973d72aec386c06d46c8803f68aa1f6936e9ee4a334adedace74e3c0'
failures=0
checks=0
skips=0
pass() { checks=$((checks + 1)); echo " ok: $1"; }
fail() { checks=$((checks + 1)); failures=$((failures + 1)); echo " FAIL: $1"; }
skip() { skips=$((skips + 1)); echo " skip: $1"; }
example="${lab_dir}/examples/webhook_demo.sh"
starter="${lab_dir}/starter/webhook_demo.sh"
echo "== Structure checks =="
[ -f "${example}" ] && pass "example script exists" || fail "example script missing"
[ -f "${starter}" ] && pass "starter script exists" || fail "starter script missing"
[ -f "${lab_dir}/starter/webhook-worksheet.md" ] && pass "worksheet exists" || fail "worksheet missing"
# The example must exercise each core step.
for needle in "openssl dgst -sha256 -hmac" "curl" "X-Webhook-Signature" "MISMATCH" "retrying"; do
if grep -q -- "${needle}" "${example}"; then pass "example uses '${needle}'"; else fail "example missing '${needle}'"; fi
done
# The starter must name its four numbered exercises.
ex_count="$(grep -cE 'Exercise [1-4]' "${starter}" 2>/dev/null || true)"
if [ "${ex_count:-0}" -ge 4 ]; then pass "starter has 4 numbered exercises"; else fail "starter has 4 numbered exercises (found ${ex_count:-0})"; fi
# Both scripts must be valid bash.
if bash -n "${example}" 2>/dev/null; then pass "example has valid bash syntax"; else fail "example has a syntax error"; fi
if bash -n "${starter}" 2>/dev/null; then pass "starter has valid bash syntax"; else fail "starter has a syntax error"; fi
echo
echo "== Local HMAC checks (no network) =="
# Recompute the signature exactly as sender and receiver would.
sig="$(printf '%s' "${PAYLOAD}" | openssl dgst -sha256 -hmac "${SECRET}" | sed 's/^.*= //')"
# It must be exactly 64 lowercase hex characters.
if printf '%s' "${sig}" | grep -qE '^[0-9a-f]{64}$'; then
pass "HMAC signature is a 64-hex-character string"
else
fail "HMAC signature is not 64 hex characters (got '${sig}')"
fi
# It must equal the known, deterministic value for this payload+secret.
if [ "${sig}" = "${EXPECTED_SIG}" ]; then
pass "HMAC signature matches the expected deterministic value"
else
fail "HMAC signature != expected (got '${sig}')"
fi
# Tampering must change the signature (integrity property).
tampered="$(printf '%s' '{"event":"invoice.paid","id":"evt_28a7","data":{"amount":999999,"currency":"usd"}}' | openssl dgst -sha256 -hmac "${SECRET}" | sed 's/^.*= //')"
if [ "${sig}" != "${tampered}" ]; then
pass "a tampered payload produces a different signature (rejected)"
else
fail "tampered payload should not match the original signature"
fi
echo
echo "== Network check (httpbin echo) =="
if ! curl -s -o /dev/null --max-time 12 "${HTTPBIN}/get" 2>/dev/null; then
skip "no network access — skipping the live delivery echo check (expected offline)"
else
echoed="$(curl -sf --max-time 25 --retry 6 --retry-delay 2 --retry-all-errors \
-X POST "${HTTPBIN}/post" \
-H 'Content-Type: application/json' \
-H "X-Webhook-Signature: sha256=${sig}" \
-d "${PAYLOAD}" 2>/dev/null)" || echoed=""
if [ -z "${echoed}" ]; then
skip "delivery check — httpbin transiently unavailable; retry later"
elif printf '%s' "${echoed}" | grep -q 'invoice.paid' \
&& printf '%s' "${echoed}" | grep -q "${sig}"; then
pass "the echo endpoint returned our payload and signature header"
else
fail "the echo endpoint should echo our payload and signature"
fi
fi
echo
echo "${checks} checks, ${failures} failure(s), ${skips} skip(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 026 lab
My signature does not match cadd3bf8...e3c0
Almost always a trailing newline. Use printf '%s' "$PAYLOAD" (not echo,
which appends \n) so the bytes you sign are exactly the payload bytes. Also
confirm you used the identical payload string and the identical secret — a
single changed byte in either flips the whole signature.
openssl: command not found
Install it (brew install openssl on macOS, sudo apt install openssl on
Debian/Ubuntu); it is preinstalled on nearly all systems. The signature step
needs it.
openssl prints HMAC-SHA256(stdin)= <hex> instead of just the hex
Different openssl builds format the label differently. The scripts strip
everything up to = with sed 's/^.*= //', which handles all the common
formats. If you are running the command by hand, pipe it through that sed
to keep only the digest.
The delivery step prints "could not reach httpbin"
httpbin.org is a shared free service and frequently returns gateway errors
or resets connections under load. The scripts already retry with backoff
(--retry ... --retry-all-errors). If it still fails, wait a minute and
re-run — the signature and verify steps do not need the network and always
work. A failed delivery is not your bug.
The test reports a skip on the network check
That is expected and fine: offline (or when httpbin is transiently down) the
live delivery check is skipped, never failed. The local HMAC checks still run
and must pass. 0 failure(s) is a pass whether there are skips or not.
The retry loop always shows 503, 503, 200
That is intentional — the loop uses a fixed, simulated sequence of statuses so the demo is deterministic and needs no flaky server to fail on purpose. It shows the shape of at-least-once retrying: keep trying on non-2xx, stop on a 2xx acknowledgement.
Windows: bash is not recognized
Use WSL (wsl --install, then open Ubuntu and follow the same steps), or run
the individual openssl and curl commands in a shell that has them.
Security notes
Security notes — Day 026 lab
- Use a fake secret. This lab's secret,
whsec_demo_do_not_use_in_production, is deliberately fake and public. A real webhook secret is a credential exactly like an API key: keep it out of source code, out of logs, and out of shared terminals. Never commit a real secret to a repository. - Verifying signatures is what prevents forged webhooks. A receiver's URL
is public — anyone who learns it can
POSTto it, including an attacker forging a "payment succeeded" event. The security is entirely in recomputing the HMAC over the body and comparing it. A receiver that skips verification trusts anyone. In production, compare signatures with a constant-time check and reject deliveries with a stale timestamp to blunt replay attacks. - Always use HTTPS for a real receiver. Serve the endpoint over HTTPS (Day 19's TLS) so the payload and signature cannot be read or altered on the wire. This lab only simulates a receiver, so it hosts nothing public.
- Never send real secrets to an echo service.
httpbin.orgechoes back whatever you send it. The lab sends only a non-sensitive sample payload and a fake signature. Do not post real tokens, keys, or personal data to any echo endpoint. - What the scripts do: compute a hash locally and make one ordinary
POSTto a public test service. Nosudo, no files written outside the lab, no data of yours sent beyond the sample payload.