Turning Your Internal Webhook Relay Script Into a Paid Integration Service
You built a webhook relay script in an afternoon to solve a boring internal problem β routing Stripe events to Slack, pushing GitHub webhooks into a Jira board, forwarding sensor payloads from one system to another. It works. You forgot about it. Then a colleague at another company asked if you could set one up for them too.
That's the moment most developers miss. What feels like a one-off glue script is actually the core of a productized service that companies will pay for every month. This article walks you through the steps to make that leap.
What You'll Learn
- How to audit your existing relay script for productization readiness
- What minimal infrastructure you need before charging anyone
- How to add authentication, logging, and rate limiting without over-engineering
- How to structure pricing for a webhook relay service
- Where to find your first paying customers
Prerequisites
This article assumes you have a working webhook relay script in any language (examples use Python and JavaScript), a basic understanding of HTTP and REST APIs, and a server or cloud account where you can deploy a small service. You don't need prior experience running a SaaS product.
Audit What You Already Have
Before writing a single new line of code, understand what your script actually does. Pull it up and answer these four questions:
- Does it filter or transform payloads? Even basic header stripping or field renaming adds value beyond a raw proxy.
- Does it retry failed deliveries? This is the feature most ad-hoc scripts skip and most paying customers want most.
- Does it log anything? A customer who can't see what happened to their webhook is a customer who files a support ticket.
- Is the destination URL hardcoded? If yes, that's the first thing to parameterize.
Write your answers down. They tell you exactly how much work stands between your script and a billable product.
The Minimum Viable Relay Service
You don't need a multi-tenant Kubernetes cluster on day one. The minimum viable version has five moving parts:
- A public HTTPS endpoint that accepts inbound webhooks. Use a simple web framework β Flask, FastAPI, or Express.
- A routing table stored somewhere persistent (a SQLite file or a Postgres table) that maps an inbound path or API key to one or more destination URLs.
- A delivery queue. Even a simple Redis list or a database table of pending jobs beats synchronous fire-and-forget, which silently drops events when the downstream is slow.
- A retry worker that picks up failed deliveries and retries with exponential backoff.
- An event log. Store the raw payload, timestamp, destination, and delivery status for every event. This is non-negotiable.
Here's a minimal FastAPI endpoint that receives a webhook and queues it for delivery:
from fastapi import FastAPI, Request, HTTPException, Header
from typing import Optional
import httpx
import asyncio
import logging
app = FastAPI()
logger = logging.getLogger(__name__)
# In production, load this from a database keyed by api_key
ROUTES = {
"key_abc123": ["https://hooks.example.com/target1"],
}
@app.post("/relay/{channel}")
async def relay_webhook(
channel: str,
request: Request,
x_api_key: Optional[str] = Header(None),
):
if x_api_key not in ROUTES:
raise HTTPException(status_code=403, detail="Invalid API key")
payload = await request.body()
headers = dict(request.headers)
destinations = ROUTES[x_api_key]
# Queue delivery β replace with a real queue in production
asyncio.create_task(deliver(payload, headers, destinations))
return {"status": "accepted"}
async def deliver(payload: bytes, headers: dict, destinations: list):
async with httpx.AsyncClient(timeout=10) as client:
for url in destinations:
try:
resp = await client.post(url, content=payload, headers=headers)
logger.info("Delivered to %s β status %s", url, resp.status_code)
except Exception as exc:
logger.error("Delivery failed to %s: %s", url, exc)
# Push to retry queue here
This is a starting point, not a finished product. The important thing is that it's shaped like a product: parameterized routing, API key auth, and async delivery in one file you can reason about.
Adding the Features Customers Actually Care About
Signature Verification
Webhook senders like Stripe, GitHub, and Shopify sign their payloads with an HMAC secret. Your relay should verify that signature before forwarding anything. This prevents your relay from becoming an open amplifier for junk traffic.
import hmac
import hashlib
def verify_stripe_signature(payload: bytes, sig_header: str, secret: str) -> bool:
timestamp, signature = None, None
for part in sig_header.split(","):
k, v = part.split("=", 1)
if k == "t":
timestamp = v
elif k == "v1":
signature = v
if not timestamp or not signature:
return False
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Build a small library of these verifiers for the most common webhook senders. It becomes a genuine selling point: customers don't have to implement verification themselves.
Retry Logic With Exponential Backoff
The most common reason a webhook delivery fails is a temporary downstream outage. A relay that retries three to five times over a few minutes solves a real problem that costs engineering hours when handled manually.
import time
async def deliver_with_retry(
payload: bytes,
headers: dict,
url: str,
max_retries: int = 5,
):
for attempt in range(max_retries):
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(url, content=payload, headers=headers)
if resp.status_code < 500:
return # Success or a 4xx the destination owns
except Exception:
pass
wait = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
await asyncio.sleep(wait)
logger.error("Permanently failed delivery to %s after %d attempts", url, max_retries)
A Self-Serve Dashboard
Customers don't want to email you to add a destination URL. Build a simple dashboard where they can manage their channels, rotate API keys, and view the event log. You don't need a polished React app to start. A server-rendered HTML interface with a table of recent events and a form to add destinations is enough to charge your first customers.
Pricing Your Service
Webhook relay is an infrastructure product, so customers think about it in terms of volume and reliability, not features. A tiered model based on events per month works well.
| Tier | Events / month | Retries | Log retention | Price |
|---|---|---|---|---|
| Starter | 25,000 | 3 attempts | 7 days | $9 / mo |
| Growth | 250,000 | 5 attempts | 30 days | $39 / mo |
| Scale | 2,000,000 | 10 attempts | 90 days | $149 / mo |
Add a free tier with a hard cap β around 1,000 events per month is common β so developers can test before committing. The free tier also drives organic growth when developers share links to tools that work.
For B2B deals with larger teams, consider an annual flat-rate plan with custom event caps. These customers will often pay several months upfront if you offer a modest discount, which helps your cash flow early on.
Infrastructure That Grows With You
Start small and upgrade when a specific constraint hurts. Here's a sensible progression:
- Phase 1: A single VPS (2 CPU, 4 GB RAM), PostgreSQL for the routing table and event log, Redis for the job queue. Handles tens of thousands of events per day without sweating.
- Phase 2: Move the delivery workers to a separate process or a managed worker service. This lets you scale workers independently of the web process when volume spikes.
- Phase 3: Add a CDN or load balancer in front of the inbound endpoint. Your ingest layer needs to be available even if your workers are catching up on a backlog.
Resist the urge to jump to microservices or Kubernetes before you have paying customers. Over-engineering before product-market fit is one of the most reliable ways to burn time and motivation.
Common Pitfalls to Avoid
Not enforcing payload size limits. A single 50 MB event payload can stall your worker queue. Set a hard limit β 5 MB is generous for most webhook use cases β and return a 413 Payload Too Large response above that threshold.
Storing raw payloads forever. Webhook payloads often contain PII or secrets. Define a retention policy and enforce it with a scheduled cleanup job. Your customers will thank you when their security team asks the question.
Logging headers without scrubbing secrets. Authorization headers and webhook signature headers contain secrets. Strip or mask them before writing to your log store.
Synchronous delivery as the default. If your relay waits for the destination to respond before returning 200 OK to the sender, a slow downstream makes your relay look broken. Always accept, queue, then deliver asynchronously.
No alerting on permanently failed deliveries. Silent failures are trust killers. Send the customer an email or a dashboard notification when an event exhausts all retries. They might not fix it immediately, but they'll know you told them.
Finding Your First Paying Customers
Your first five customers are almost certainly in your existing network. Start there, not with cold outreach to strangers.
- Post in developer Slack communities and Discord servers where people discuss the specific tools your relay supports (Stripe, GitHub, Shopify, Linear, etc.). A short message like
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!