Building a Secure Webhook Receiver for Server-to-Server Communication
How I built a secure FastAPI webhook receiver using RSA signatures to enable authenticated server-to-server communication, proxy requests, and connect applications across different network environments.
Aug 11, 2026 · 7 min read
You have an application running on one server, but the service you need to communicate with is only accessible from another network. Maybe the destination API is blocked from your server, maybe it only accepts requests from specific IP ranges, or maybe your production infrastructure is located outside the country where the service operates.
That was exactly the kind of problem I faced while working on one of my projects.
So instead of building another application-specific workaround, I decided to create a small, reusable service: Signed Webhook Receiver.
It is a lightweight webhook gateway built with FastAPI, designed to receive requests from another server, verify their authenticity using RSA digital signatures, and then process or forward the request.
The problem I wanted to solve
One of the services I was working on needed to communicate with Telegram.
The problem was that my application server was located inside Iran, and I couldn't reliably make the required requests to Telegram from that server.
Instead of moving the entire application to another server, I needed a small intermediate service that could receive a request from my application and perform the external communication from a server with the required network access.
The architecture became something like this:
My Application
|
| Signed HTTP Request
v
Signed Webhook Receiver
|
| External HTTP Request
v
External Service
This approach has another important advantage: the intermediate server doesn't have to trust every request that reaches it.
The sender signs the payload using a private RSA key, while the receiver only stores the corresponding public key.
If the signature is invalid, the request is rejected.
What is Signed Webhook Receiver?
Signed Webhook Receiver is a minimal server-to-server communication gateway built with Python, FastAPI, Uvicorn, Cryptography, Docker, and Traefik.
The main idea is simple:
Receive a request, verify its signature, and only then accept the payload.
The project uses RSA-SHA256 signatures for message authentication and integrity. The sender signs the message with its private key, and the receiver verifies the signature using the public key.
This means the receiver doesn't need to expose a traditional password or API key for authenticating the sender.
How does it work?
Server A has the private key, while Server B only has the public key.
When Server A wants to send a message, it creates a canonical representation of the payload and signs it using RSA-SHA256.
For example:
{
"event": "payment.completed",
"id": 123
}
The signature is then encoded and sent together with the payload:
{
"sign": "BASE64_SIGNATURE",
"data": {
"event": "payment.completed",
"id": 123
}
}
The receiver recreates the exact message that was signed and verifies the signature.
Request
↓
Read payload
↓
Verify RSA signature
↓
Valid?
┌─┴─────────┐
Yes No
↓ ↓
Process Reject
The current implementation exposes POST /api/v1/webhook.
A successfully verified request receives a successful response, while an invalid signature is rejected.
Why RSA signatures?
There are many ways to authenticate server-to-server requests, including API keys, HMAC, JWT, OAuth, mTLS, and digital signatures.
For this project, I wanted a model where the receiving server doesn't need to know or store a shared secret.
The sender owns the private key. The receiver owns the public key.
The receiver can therefore verify that the message was signed by someone possessing the private key and that the message hasn't been modified after it was signed.
It is not only a webhook receiver
Although the project is called Signed Webhook Receiver, the underlying architecture is useful beyond traditional webhook events.
It can act as a small server-to-server gateway.
This can be useful when your main application cannot directly access a destination service. The intermediate server becomes the controlled boundary between your application and the external service.
Using it as a proxy
One possible use case is building a controlled HTTP proxy.
Suppose your application needs to call an external API, but your application server doesn't have access to that API. You can deploy the receiver somewhere that has access and send authenticated requests through it.
The important part is that this should be implemented as an allowlisted, purpose-specific proxy, rather than turning the server into an open proxy.
You should restrict allowed destinations, HTTP methods, request size, authentication, rate limits, timeouts, logging, and allowed source applications.
Otherwise, an exposed proxy can quickly become a security problem.
Another interesting use case: Iranian payment gateways
This architecture can also be useful for applications that need to communicate with Iranian payment providers or banking services that require requests to originate from an Iranian IP address.
At the same time, many production applications are deployed on infrastructure outside Iran.
For example:
Production Server Outside Iran
|
| Signed Request
v
Iranian Gateway Server
|
| Payment API
v
Iranian Payment Gateway
The intermediate server acts as the network boundary. The application doesn't need to move its entire infrastructure into Iran; it sends an authenticated request to the Iranian server, and that server communicates with the payment provider.
This should only be used where the payment provider's terms and technical requirements permit such an architecture. The purpose is to solve a legitimate network-placement requirement, not to bypass authentication, fraud controls, sanctions controls, or other security mechanisms.
Why not simply expose an API?
You could expose a normal endpoint such as POST /proxy and accept requests from anywhere. But that creates a major security problem.
If your intermediate server is publicly accessible, anyone who discovers the endpoint could potentially use it.
With signed requests, the receiver can distinguish between a trusted application with a valid signature and an unknown client with an invalid signature.
The private key never needs to exist on the receiver. That is one of the main security properties of this architecture.
Key management
The project uses a public/private RSA key pair.
Generate the private key:
openssl genrsa -out private.pem 2048
Generate the public key:
openssl rsa -in private.pem -pubout -out public.pem
The sender keeps private.pem, while the receiver gets public.pem.
The private key should never be deployed to the receiving server. If someone obtains the private key, they can generate valid signatures and impersonate the sender.
Security considerations
HTTPS
Always use HTTPS in production. A signature provides authenticity and integrity of the signed message, but it doesn't replace transport encryption.
Prevent replay attacks
A valid signature can potentially be replayed if the application doesn't include freshness information.
For production systems, payloads should include values such as a timestamp, nonce, or unique request ID. The receiver can then reject requests that are too old or have already been processed.
Canonicalize the payload
Both sides must sign and verify exactly the same representation of the message. For example:
json.dumps(
data,
separators=(",", ":"),
sort_keys=True
)
This prevents differences in JSON formatting from producing different signatures.
Don't create an open proxy
If you use the project as a proxy, don't allow arbitrary destinations such as POST /proxy?url=https://anything.example without strict validation.
A better architecture is to define specific allowed operations such as /payment/request, /payment/verify, /telegram/send, and /telegram/edit, and let the server decide where each operation is forwarded.
Technology Stack
The project is intentionally small:
Python 3.12
FastAPI
Uvicorn
Cryptography
Docker
Traefik
FastAPI handles the HTTP layer, while the cryptography package handles RSA signature verification. Docker makes the service easy to deploy as an independent microservice, and Traefik can sit in front of it as the reverse proxy and TLS entry point.
Why I built it
I built Signed Webhook Receiver because I needed it.
I had an application that needed to communicate with Telegram, but the server running my application was inside Iran and I couldn't make the required requests reliably from there.
Instead of coupling a network workaround directly into my application, I extracted the problem into a small independent service.
That gave me something reusable for Telegram, external APIs, payment services, and other server-to-server communication scenarios.
Final thoughts
Sometimes infrastructure problems don't require a huge architecture.
A small, well-defined intermediate service can solve a very specific networking problem while keeping the main application clean.
Signed Webhook Receiver is my implementation of that idea:
A lightweight, secure gateway for authenticated server-to-server communication.
The project is open source and available on GitHub: signed-webhook-receiver.
Related Notes
Why I Switched to Conventional Git Commit Messages
Why I switched from inconsistent Git commit messages to a Conventional Commits style, with practical examples for cleaner and more maintainable Git history.
Building django-healthkit
How a simple health endpoint for Bidar turned into django-healthkit, a lightweight Django health-check package.
Designing a Gold Jewelry E-Commerce Database with Django
Learn how to design a scalable Django database for a gold jewelry e-commerce platform by modeling products, attributes, and purchasable product variants using real-world domain-driven design principles.
How to Fix Common Next.js 16 Build Errors (Proxy, Dynamic Rendering & Revalidation)
Learn how to fix common Next.js 16 build errors, understand Proxy, Dynamic Rendering, and Revalidation, and improve your application's SEO and performance.
Why I Built cyber-ui: A Minimal Design System for Developers
How my projects, from backend systems to monitoring tools, led me to create a minimal and personal UI foundation.