WebHook Tester
Overview
WebHook Tester is an open source, self-hosted endpoint for capturing, inspecting and debugging inbound HTTP requests. You create a session, which gives you a unique capture URL, and point a third-party service at it. Every request that arrives is recorded, so you can read back the exact method, headers and body that were actually sent.
This guide covers deploying and configuring WebHook Tester on Ubuntu 24.04 on Azure, using the SecureAnyCloud Azure Marketplace image. It answers the question every webhook integration eventually raises: what did that service really send me? Instead of guessing from documentation, you point the sender at a capture URL and read the truth off the wire.
The image installs the official WebHook Tester binary (pinned and checksum-verified at build). It runs as the non-root webhook-tester user under systemd, bound to loopback behind an nginx reverse proxy on port 80, so a capture endpoint is listening within minutes of launch.
What you get:
- WebHook Tester (official pinned binary), run under systemd as the unprivileged
webhook-testeruser (webhook-tester.service). - An nginx reverse proxy on port 80 in front of the loopback-bound service (
nginx.service). - The filesystem storage driver, so captured requests persist across reboots with no bundled database. WebHook Tester supports memory, filesystem and Redis storage. With the filesystem driver, there is no database to run, secure or patch.
- A per-VM web UI password, generated on first boot and written to a root-only credentials file. It is never baked into the image.
- A shipped round-trip self test that proves an anonymous POST is captured and readable back, and that unauthenticated reads are rejected.
- Retention guard rails: a 7-day session lifetime, 128 requests per session and a 1 MiB request body cap.
- Unattended security upgrades left enabled, so the server keeps receiving patches.
Security by design: the auth split. A webhook receiver has an unusual requirement. The capture URL must accept unauthenticated requests, because that is the entire point. Everything else must not be world-readable. This image mirrors WebHook Tester's own routing rule in nginx:
- Capture URLs are anonymous. A request whose first path segment is a session UUID is a capture. Any third-party service can POST to
http://<your-vm-ip>/<session-uuid>with no credentials, and the request is recorded. - The web UI and the management API require a password. Creating sessions, listing them and reading captured history are all behind HTTP Basic auth. The credential is generated uniquely on this VM's first boot and never baked into the image. An unauthenticated request returns HTTP 401.
- Nobody can mint endpoints on your instance. Automatic session creation is left off, so a session can be created only through the authenticated API or UI. An anonymous caller can post to an endpoint you already made, but cannot create new ones.
- Treat a capture URL as a capability. So that the UI's live feed works, the live-update WebSocket for a session accepts an unauthenticated handshake. Browsers cannot send credentials on a WebSocket handshake, so requiring auth there would permanently break live updates. Whoever holds a session UUID can therefore both post to that endpoint and watch that one session's live stream. They still cannot create sessions, list them, read captured history over the REST API, or open the web UI. If different senders must not see each other's traffic, give each one its own session.
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet and subnet in the target region.
- A subscription to the WebHook Tester listing on Azure Marketplace.
- Network Security Group rules that allow TCP 22 (admin) and TCP 80 (WebHook Tester via nginx) from the services that will post to your capture URLs.
- Recommended virtual machine size:
Standard_B2s(2 vCPU, 4 GB RAM), which is ample for debugging and integration work. Increase the size only if you expect sustained high-volume capture traffic.
Deploy from the Azure Portal
Search the Marketplace for WebHook Tester, select the SecureAnyCloud publisher, and click Create.
Configure the Network Security Group to allow TCP 80 from the services that will send you webhooks, and TCP 22 for administration. For internet exposure, front port 80 with TLS (see Step 8).
Deploy from the Azure CLI
RG="webhook-tester-prod"; LOCATION="eastus"; VM_NAME="webhooktester1" IMAGE_URN="sac1647283583153:webhook-tester-ubuntu-24-04:default:latest" SSH_KEY="$(cat ~/.ssh/id_rsa.pub)" az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:webhook-tester-ubuntu-24-04:default:latest
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$IMAGE_URN" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard \
--os-disk-delete-option Delete --nic-delete-option Delete
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 80 --priority 1001First boot
On first boot, the image generates a unique web UI password for this VM, writes it to a root-only credentials file, and arms the service. Nothing is shared between VMs. Two machines launched from the same image get different passwords.
Read your credentials over SSH:
sudo cat /root/webhook-tester-credentials.txt# WebHook Tester — Per-VM Instance Info
# Generated: Sun Jul 19 07:13:34 UTC 2026
#
# There is NO default password. The value below was generated uniquely on THIS
# VM's first boot and was never baked into the image. Keep this file secret (root only). # webhooktester.host=<your-vm-ip> webhooktester.url=http://<your-vm-ip>/ webhooktester.username=admin webhooktester.password=****Confirm the server is running
systemctl is-active webhook-tester nginx /usr/local/bin/webhook-tester --version ss -tlnactive active webhook-tester version 2.3.0 (go1.26.2)The listener set is deliberately small. nginx on port 80 and sshd on port 22 are the only externally bound services, and WebHook Tester itself is reachable only on loopback.
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("webhook-tester",...)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",...)) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...))Run the round trip self test
The image ships a self test that proves the product captures data, rather than merely that a port is open. The test creates a session through the authenticated API and posts a known payload to the capture URL with no credentials. It reads the request back through the authenticated API and asserts that the exact payload and header survived. Finally, it checks that unauthenticated reads are refused.
sudo bash /usr/local/lib/sac/webhook-tester-roundtrip.shROUNDTRIP OK: created a capture session through the authenticated API; an ANONYMOUS POST to the capture URL was accepted (200) and the exact payload plus its custom header were read back through the authenticated API; the same read without credentials was rejected 401, a wrong password was rejected 401, and the web UI root was rejected 401. The live-update WebSocket upgraded (101) while a plain unauthenticated GET to the same path was rejected 401. Data capture and the auth split are both proven end to end through nginx.Capture a webhook yourself
First, create a capture session through the authenticated management API. Substitute your own password from Step 3.
PASSWORD="<your-password-from-step-3>" curl -sS -u "admin:$PASSWORD" -X POST http://127.0.0.1/api/session \
-H 'Content-Type: application/json' \
-d '{"status_code":200,"headers":[],"delay":0,"response_body_base64":""}'{ "created_at_unix_milli": 1784445686996, "response": { "delay": 0, "headers": [], "response_body_base64": "", "status_code": 200 }, "uuid": "b6946d26-430a-4c9c-b98f-d141858c2def" }The uuid is your capture endpoint. Your capture URL is http://<your-vm-ip>/<uuid>. Give this address to the service that will send you webhooks.
Now post to it the way a third party would, with no credentials at all:
SID="<the-uuid-from-above>" curl -sS -o /dev/null -w 'HTTP %{http_code}\n' -X POST "http://127.0.0.1/$SID" \
-H 'Content-Type: application/json' \
-H 'X-Demo-Source: order-service' \
-d '{"event":"order.created","order_id":"A-1024","total":49.99}'HTTP 200Read the captured request back through the authenticated API:
curl -sS -u "admin:$PASSWORD" "http://127.0.0.1/api/session/$SID/requests"The response records the exact method, the client address, every header the caller sent (including your custom X-Demo-Source), and the body, base64-encoded:
[ { "captured_at_unix_milli": 1784445687047, "client_address": "127.0.0.1", "headers": [ { "name": "Content-Type", "value": "application/json" }, { "name": "User-Agent", "value": "curl/8.5.0" }, { "name": "X-Demo-Source", "value": "order-service" } ], "method": "POST", "request_payload_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQiLCJvcmRlcl9pZCI6IkEtMTAyNCIsInRvdGFsIjo0OS45OX0=", "url": "http://127.0.0.1/b6946d26-430a-4c9c-b98f-d141858c2def" } ]Decoding request_payload_base64 returns the payload byte for byte as it was sent:
echo 'eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQiLCJvcmRlcl9pZCI6IkEtMTAyNCIsInRvdGFsIjo0OS45OX0=' | base64 -d{"event":"order.created","order_id":"A-1024","total":49.99}Use the web UI
Browse to http://<your-vm-ip>/ and sign in with admin and the password from Step 3. The UI shows your current capture URL, ready-made snippets for curl, wget, HTTPie, PowerShell and several languages, and the response the endpoint will return to callers.
Requests appear in the left-hand column live, as they arrive, with no page reload. The image ships the WebSocket proxying that makes this work.
Select any request to see its full detail: path, method, originating address, timestamp, size, the complete header set, and the body rendered as text or binary.
Expand Show all headers to see every header exactly as the sender transmitted it. This is usually where webhook debugging gets resolved: signature headers, content types and custom event fields.
Front the server with TLS for internet exposure
Capture URLs are anonymous by design, so anything sent to them travels in the clear over plain HTTP. If your capture URLs are reachable from the internet, terminate TLS in front of nginx. This encrypts webhook payloads and your UI password in transit.
Point a DNS A record at the VM's public IP. Then use certbot's nginx plugin, which edits the shipped server block in place and sets up renewal:
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d webhooks.example.comCertbot offers to redirect HTTP to HTTPS. Accept the offer, then re-issue your capture URLs as https://webhooks.example.com/<uuid>.
Security recommendations
- Restrict port 80/443 where you can. Most webhook providers publish their egress IP ranges. Scope the Network Security Group to those ranges instead of leaving the capture port open to the whole internet.
- Give each sender its own session. Anyone holding a session UUID can post to that endpoint and watch its live stream. Separate sessions keep separate senders from seeing each other's traffic.
- Terminate TLS before you expose capture URLs to the internet (Step 8).
- Keep the credentials file root-only. It is written
0600 root:root. Leave it that way, and rotate the password if it is ever shared. - Treat captured data as sensitive. Webhooks routinely carry customer records, tokens and signatures. Captured requests expire after 7 days, and you can clear them at any time with Delete all requests in the UI.
- Leave unattended upgrades enabled so the VM keeps receiving security patches.
Support and Licensing
WebHook Tester is open source software distributed under the MIT Licence. SecureAnyCloud packages it as a preconfigured Azure image. The software itself remains under its upstream licence and is available from the upstream project.
For image-specific issues, contact SecureAnyCloud support at sales@secureanycloud.com. For questions about WebHook Tester itself, consult the upstream project documentation.
Deploy on Azure
Launch WebHook Tester on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
WebHook Tester is an open source, self-hosted endpoint for capturing, inspecting and debugging inbound HTTP requests. You create a session, which gives you a unique capture URL, and point a third-party service at it. Every request that arrives is recorded, so you can read back the exact method, headers and body that were actually sent.
This guide covers deploying and configuring WebHook Tester on AWS, using the SecureAnyCloud AWS Marketplace AMI. It answers the question every webhook integration eventually raises: what did that service really send me? Instead of guessing from documentation, you point the sender at a capture URL and read the truth off the wire.
The image installs the official WebHook Tester binary (pinned and checksum-verified at build). It runs as the non-root webhook-tester user under systemd, bound to loopback behind an nginx reverse proxy on port 80, so a capture endpoint is listening within minutes of launch.
What you get:
- WebHook Tester (official pinned binary), run under systemd as the unprivileged
webhook-testeruser (webhook-tester.service). - An nginx reverse proxy on port 80 in front of the loopback-bound service (
nginx.service). - The filesystem storage driver, so captured requests persist across reboots with no bundled database. WebHook Tester supports memory, filesystem and Redis storage. With the filesystem driver, there is no database to run, secure or patch.
- A per-instance web UI password, generated on first boot and written to a root-only credentials file. It is never baked into the image.
- A shipped round-trip self test that proves an anonymous POST is captured and readable back, and that unauthenticated reads are rejected.
- Retention guard rails: a 7-day session lifetime, 128 requests per session and a 1 MiB request body cap.
- Unattended security upgrades left enabled, so the server keeps receiving patches.
Security by design: the auth split. A webhook receiver has an unusual requirement. The capture URL must accept unauthenticated requests, because that is the entire point. Everything else must not be world-readable. This image mirrors WebHook Tester's own routing rule in nginx:
- Capture URLs are anonymous. A request whose first path segment is a session UUID is a capture. Any third-party service can POST to
http://<public-ip>/<session-uuid>with no credentials, and the request is recorded. - The web UI and the management API require a password. Creating sessions, listing them and reading captured history are all behind HTTP Basic auth. The credential is generated uniquely on this instance's first boot and never baked into the image. An unauthenticated request returns HTTP 401.
- Nobody can mint endpoints on your instance. Automatic session creation is left off, so a session can be created only through the authenticated API or UI. An anonymous caller can post to an endpoint you already made, but cannot create new ones.
- Treat a capture URL as a capability. So that the UI's live feed works, the live-update WebSocket for a session accepts an unauthenticated handshake. Browsers cannot send credentials on a WebSocket handshake, so requiring auth there would permanently break live updates. Whoever holds a session UUID can therefore both post to that endpoint and watch that one session's live stream. They still cannot create sessions, list them, read captured history over the REST API, or open the web UI. If different senders must not see each other's traffic, give each one its own session.
Connecting to your instance
WebHook Tester is offered as one master listing with an AMI variant per operating system. Connect over SSH as the default login user for the variant you launched.
- Ubuntu 24.04 LTS: SSH login user ubuntu. nginx reverse proxy on port 80, with service webhook-tester.service.
ssh ubuntu@<public-ip>Prerequisites
- An AWS account subscribed to the WebHook Tester listing on AWS Marketplace.
- A VPC with a public subnet in your target region, and an EC2 key pair for SSH.
- A security group that allows inbound TCP 22 (admin) and TCP 80 (WebHook Tester via nginx) from the services that will post to your capture URLs.
- Recommended instance type:
m5.large(2 vCPU, 8 GB RAM), which is ample for debugging and integration work. Increase the size only if you expect sustained high-volume capture traffic.
Deploy from AWS Marketplace
Find the WebHook Tester listing in AWS Marketplace and choose the SecureAnyCloud publisher. Click Continue to Subscribe, then Continue to Configuration and Continue to Launch.
Pick your instance type, VPC subnet and key pair. Configure the security group to allow TCP 80 from the services that will send you webhooks, and TCP 22 for administration. For internet exposure, front port 80 with TLS (see Step 8).
Deploy from the AWS CLI
aws ec2 run-instances \
--image-id <ami-id> \
--instance-type m5.large \
--key-name <your-key-pair> \
--security-group-ids <your-sg-id> \
--subnet-id <your-public-subnet-id> \
--associate-public-ip-address \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=webhook-tester}]'The Marketplace launch page shows the AMI id for your chosen OS variant and region. Open TCP 80 and TCP 22 in the referenced security group.
First boot
On first boot, the image generates a unique web UI password for this instance and resolves the instance public IPv4 via EC2 IMDSv2. It writes both to a root-only credentials file and arms the service. Nothing is shared between instances. Two machines launched from the same AMI get different passwords.
Read your credentials over SSH:
sudo cat /root/webhook-tester-credentials.txt# WebHook Tester - Per-VM Instance Info
# Generated: Tue Jul 21 21:30:00 UTC 2026
#
# There is NO default password. The value below was generated uniquely on THIS
# instance's first boot and was never baked into the image. Keep this file secret (root only). # webhooktester.host=<public-ip> webhooktester.url=http://<public-ip>/ webhooktester.username=admin webhooktester.password=****Confirm the server is running
systemctl is-active webhook-tester nginx /usr/local/bin/webhook-tester --version ss -tlnactive active webhook-tester version 2.3.0 (go1.26.2)The listener set is deliberately small. nginx on port 80 and sshd on port 22 are the only externally bound services, and WebHook Tester itself is reachable only on loopback port 8080.
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("webhook-tester",...)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",...)) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",...))Run the round trip self test
The image ships a self test that proves the product captures data, rather than merely that a port is open. The test creates a session through the authenticated API and posts a known payload to the capture URL with no credentials. It reads the request back through the authenticated API and asserts that the exact payload and header survived. Finally, it checks that unauthenticated reads are refused.
sudo bash /usr/local/lib/sac/webhook-tester-roundtrip.shROUNDTRIP OK: created a capture session through the authenticated API; an ANONYMOUS POST to the capture URL was accepted (200) and the exact payload plus its custom header were read back through the authenticated API; the same read without credentials was rejected 401, a wrong password was rejected 401, and the web UI root was rejected 401. The live-update WebSocket upgraded (101) while a plain unauthenticated GET to the same path was rejected 401. Data capture and the auth split are both proven end to end through nginx.Capture a webhook yourself
The following self-contained snippet reads the per-instance password from the credentials file and creates a capture session through the authenticated management API. It then posts a request to the session the way a third-party service would, with no credentials at all, and reads the captured request back. Run it on the instance over SSH:
PASSWORD="$(sudo grep '^webhooktester.password=' /root/webhook-tester-credentials.txt | cut -d= -f2-)" SID="$(curl -sS -u "admin:$PASSWORD" -X POST http://127.0.0.1/api/session \
-H 'Content-Type: application/json' \
-d '{"status_code":200,"headers":[],"delay":0,"response_body_base64":""}' \
| grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' | head -1)" echo "Capture session: $SID" curl -sS -o /dev/null -w 'anonymous POST -> HTTP %{http_code}\n' -X POST "http://127.0.0.1/$SID" \
-H 'Content-Type: application/json' \
-H 'X-Demo-Source: order-service' \
-d '{"event":"order.created","order_id":"A-1024","total":49.99}' curl -sS -u "admin:$PASSWORD" "http://127.0.0.1/api/session/$SID/requests"The SID is your capture endpoint. Your capture URL is http://<public-ip>/<SID>. Give this address to the service that will send you webhooks. The anonymous POST returns HTTP 200. The authenticated read records the exact method, the client address, every header the caller sent (including your custom X-Demo-Source), and the body, base64-encoded:
[ { "captured_at_unix_milli": 1784445687047, "client_address": "127.0.0.1", "headers": [ { "name": "Content-Type", "value": "application/json" }, { "name": "User-Agent", "value": "curl/8.5.0" }, { "name": "X-Demo-Source", "value": "order-service" } ], "method": "POST", "request_payload_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQiLCJvcmRlcl9pZCI6IkEtMTAyNCIsInRvdGFsIjo0OS45OX0=", "url": "http://127.0.0.1/<SID>" } ]Decoding request_payload_base64 returns the payload byte for byte as it was sent.
Use the web UI
Browse to http://<public-ip>/ and sign in with admin and the password from Step 3. The UI shows your current capture URL, ready-made snippets for curl, wget, HTTPie, PowerShell and several languages, and the response the endpoint will return to callers.
Requests appear in the left-hand column live, as they arrive, with no page reload. The image ships the WebSocket proxying that makes this work.
Select any request to see its full detail: path, method, originating address, timestamp, size, the complete header set, and the body rendered as text or binary.
Expand Show all headers to see every header exactly as the sender transmitted it. This is usually where webhook debugging gets resolved: signature headers, content types and custom event fields.
Front the server with TLS for internet exposure
Capture URLs are anonymous by design, so anything sent to them travels in the clear over plain HTTP. If your capture URLs are reachable from the internet, terminate TLS in front of nginx. This encrypts webhook payloads and your UI password in transit.
Point a DNS A record at the instance's public IP. Then use certbot's nginx plugin, which edits the shipped server block in place and sets up renewal:
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d webhooks.example.comCertbot offers to redirect HTTP to HTTPS. Accept the offer, then re-issue your capture URLs as https://webhooks.example.com/<session-uuid>.
Security recommendations
- Restrict port 80 and 443 where you can. Most webhook providers publish their egress IP ranges. Scope the security group to those ranges instead of leaving the capture port open to the whole internet.
- Give each sender its own session. Anyone holding a session UUID can post to that endpoint and watch its live stream. Separate sessions keep separate senders from seeing each other's traffic.
- Terminate TLS before you expose capture URLs to the internet (Step 8).
- Keep the credentials file root-only. It is written
0600 root:root. Leave it that way, and rotate the password if it is ever shared. - Treat captured data as sensitive. Webhooks routinely carry customer records, tokens and signatures. Captured requests expire after 7 days, and you can clear them at any time with Delete all requests in the UI.
- Leave unattended upgrades enabled so the instance keeps receiving security patches.
Support and Licensing
WebHook Tester is open source software distributed under the MIT License. SecureAnyCloud packages it as a preconfigured AWS image. The software itself remains under its upstream license and is available from the upstream project.
For image-specific issues, contact SecureAnyCloud support at sales@secureanycloud.com. For questions about WebHook Tester itself, consult the upstream project documentation.
Deploy on AWS
Launch WebHook Tester with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com