Getting Started with Webhooks: Part 2 - Webhook Clients

A comprehensive two-part guide to understanding webhooks; this part focuses on webhook clients.

In Part 1 of this series, we covered webhook servers, what they are, how they work, and subsequently deployed a simple webhook server using Node.js and Express on Railway. In this part, we'll focus on webhook clients, what they are, how they interact with webhook servers, and then deploy a simple webhook client using Streamlit on Railway.

Getting Started with Webhook Clients

A webhook client is the system that sends webhook requests when certain events occur. It constructs an HTTP request and sends it to the webhook server's registered URL. Here are the general steps to be followed when setting up a webhook client:

  1. Identify relevant events: Determine which events should trigger webhook requests.
  2. Generate webhook payloads: When an event occurs, create a serialised form-encoded JSON payload with relevant data.
  3. Send webhook requests: Construct an HTTP POST request with the JSON payload and send it to the webhook server's URL.

What We're Building - The Webhook Client

The webhook client in this series:

  • Accepts a Webhook URL, event type, JSON payload, and optional shared secret
  • Signs the payload with HMAC-SHA256 if a secret is provided, using the same algorithm as the server
  • Sends the request and displays the headers sent, payload sent, and server response β€” all in expandable panels

The client is built with Streamlit, with additional features described below. Here's an excerpt from the app.py; you can find the complete source code here.

import hmac
import hashlib
import json
import requests
import streamlit as st

st.set_page_config(page_title="Webhook Client", page_icon="πŸͺ")
st.subheader("Webhook Client")
st.caption("Send signed or unsigned HTTP POST requests to any webhook endpoint.")

with st.form("webhook_form"):
    url = st.text_input(
        "Webhook URL",
        value="http://webhook-server.railway.internal/webhook/test",
        help="The full URL of the webhook endpoint to send the request to.",
    )
    event_type = st.text_input(
        "Event Type",
        value="user.created",
        help="Sent as the X-Event-Type header. Useful for routing events on the server.",
    )
    secret = st.text_input(
        "Webhook Secret (optional)",
        type="password",
        help="If provided, the payload will be signed with HMAC-SHA256 and sent as X-Webhook-Signature.",
    )
    payload = st.text_area(
        "JSON Payload",
        value='{\n  "id": 1,\n  "name": "Alice"\n}',
        height=200,
        help="Must be valid JSON.",
    )
    submitted = st.form_submit_button("Submit")

if submitted:
    if not url.strip():
        st.error("Please provide a Webhook URL.")
    else:
        try:
            parsed_payload = json.loads(payload)
        except json.JSONDecodeError:
            st.error("Invalid JSON payload. Please check your input.")
            st.stop()

        headers = {"Content-Type": "application/json"}

        if event_type.strip():
            headers["X-Event-Type"] = event_type.strip()

        if secret.strip():
            body = json.dumps(parsed_payload)
            sig = hmac.new(
                secret.strip().encode(),
                body.encode(),
                hashlib.sha256,
            ).hexdigest()
            headers["X-Webhook-Signature"] = f"sha256={sig}"

        try:
            response = requests.post(url, json=parsed_payload, headers=headers, timeout=10)
            st.success(f"Response: {response.status_code} {response.reason}")

            st.markdown("**Request**")
            with st.expander("Headers sent", expanded=True):
                st.json(dict(headers))
            with st.expander("Payload sent", expanded=True):
                st.json(parsed_payload)

            st.markdown("**Response**")
            content_type = response.headers.get("Content-Type", "")
            with st.expander("Response body", expanded=True):
                if "application/json" in content_type:
                    st.json(response.json())
                else:
                    st.text(response.text)

        except requests.exceptions.ConnectionError:
            st.error("Connection failed. Check the Webhook URL and ensure the server is running.")
        except requests.exceptions.Timeout:
            st.error("Request timed out after 10 seconds.")
        except Exception as e:
            st.error(f"Unexpected error: {str(e)}")

Deploying the Webhook Client on Railway

You can deploy the webhook client with the one-click Railway template:

Deploy on Railway

Once the deployment completes, the webhook client will be available at a default xxx.up.railway.app domain - launch this URL and send your first webhook! If you are interested in setting up a custom domain, I covered it at length in a previous post - see the final section here.

Simple webhook client deployed on Railway
Simple webhook client deployed on Railway

The client's railway.toml tells Railway how to start the Streamlit app and which port to bind to:

[deploy]
startCommand = "streamlit run app.py --server.port $PORT --server.address 0.0.0.0"

Change the Webhook URL in the UI to https://webhook-server.up.railway.app/webhook/user.created (or your custom domain), set the secret to match WEBHOOK_SECRET on your local server (if configured), add a payload, and send a request. You'll see the signed headers in the Headers sent expander and the server's response in Response body.

A Few Thoughts

HMAC-SHA256 Signing

The client uses Python's hmac module to sign the payload using the same algorithm the server uses to verify it. The signature is computed over the JSON-serialised payload body and sent as:

X-Webhook-Signature: sha256=<hex_digest>

Signing is entirely optional though. If the secret field is left blank, no signature header is sent. This matches the server's behaviour: if WEBHOOK_SECRET is not set on the server, all requests are accepted regardless of whether they're signed.

Event Type Header

The X-Event-Type header is sent alongside the payload to communicate what kind of event this is. On the server side, this is logged and echoed back in the response. In real webhook systems (GitHub, Stripe, etc.), event type headers are how receivers route events to the right handler without inspecting the payload.

Request/Response Display

The client shows three expandable panels after a successful request:

  • Headers sent β€” shows the full set of headers including the signature if one was generated
  • Payload sent β€” confirms exactly what JSON was sent
  • Response body β€” shows the server's JSON response or raw text

This makes it easy to see the full round-trip and debug mismatches between what was sent and what the server received.

Security, Reliability and Other Considerations

Now that we've explored the concept of webhooks, common use cases, differences between webhook servers and clients, and deployed them, let's review the security, reliability, and other deployment considerations.

  1. Security: Webhooks can expose sensitive data if not properly secured. Use HTTPS, authenticate requests, and validate timestamps/payloads to enhance security. Mutual TLS authentication can also be used for added security.
  2. Data privacy and compliance: Review legal and regulatory requirements around data handling and sharing, especially PII data. The use of webhooks must comply with applicable regulations to avoid fines and legal liabilities.
  3. Reliability: Webhook requests can fail due to network issues or server downtime. Implement error handling and retry mechanisms, fail gracefully, and log requests to improve reliability.
  4. Payload size: Large payloads can cause performance issues. Optimize payloads by including only necessary data. Generally, webhooks are good for lightweight, specific actions and payloads.
  5. Rate limiting: Some webhook servers may impose rate limits. Be aware of these limits and adjust your webhook client accordingly.
  6. Monitoring: Monitor your webhook infrastructure to detect and troubleshoot issues quickly.
  7. Finally, not all applications support webhook integrations. Review the available APIs to determine the best method for communications.

Subscribe to alphasec

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe