If you have ever built a test suite or automation script that waits for an email, you know the drill: write a polling loop, set an interval, hope the email arrives before your timeout fires, and deal with the wasted requests every time the inbox is empty. It works, but it is wasteful, fragile, and annoying to maintain.
FreeCustom.Email now supports webhooks — HTTP push notifications delivered to your server the instant a new email arrives in any registered inbox. No polling. No guessing. Just an HTTP POST to your endpoint the moment the message lands.
This guide explains how the FreeCustom.Email webhook system works, when to use it versus long-polling or WebSocket, how to set it up, how to verify signatures, and includes complete working examples in Node.js, Python, Go, and PHP.
What Is a Webhook?
A webhook is a reverse API. Instead of your code repeatedly asking "is there a new email?" (polling), the FCE server asks your code "an email just arrived, here's the data" (push). You register a publicly accessible HTTPS URL with us, and we send an HTTP POST to that URL every time a new message hits an inbox you're subscribed to.
Webhooks are ideal for server-to-server event notifications and are the backbone of payment systems (Stripe), source control notifications (GitHub), SMS delivery receipts (Twilio), and now — disposable email pipelines.
Webhooks vs. Long-Polling vs. WebSocket — When to Use Each
FreeCustom.Email offers three ways to receive real-time email notifications. Choosing the right one matters:
Method | Initiator | Best for | Plan required |
|---|---|---|---|
Polling (basic GET) | Your code | Simple scripts, low frequency | Free |
Long-polling ( | Your code | Agents, CLI tools, short-lived scripts | Developer+ |
WebSocket ( | Your code | Browser apps, live dashboards | Startup+ |
Webhooks | FCE server | Persistent backends, n8n, Zapier, CI | Growth+ |
Use webhooks when:
You have a persistent backend server with a public HTTPS endpoint
Events are infrequent or unpredictable (webhooks send data only when something happens)
You want to trigger downstream automation — database writes, Slack alerts, test assertions
You're integrating with n8n, Zapier, Make, or any workflow automation platform
Use long-polling when:
You're writing a short-lived script or CLI tool that needs to wait for one email
You don't have a public server endpoint (your process initiates the connection)
You're building an MCP-based AI agent that calls
create_and_wait_for_otp
Use WebSocket when:
You need a browser-facing live inbox interface
You want bidirectional, persistent connection management
For most backend automation and CI/CD scenarios, webhooks are the cleanest answer — they eliminate polling entirely and integrate naturally with every workflow automation tool available today.
Setting Up Webhooks
Step 1: Get a Growth API key
Webhooks require the Growth ($49/mo) or Enterprise ($149/mo) plan. Get your key from the API dashboard.
Step 2: Register an inbox
curl -X POST https://api2.freecustom.email/v1/inboxes \
-H "Authorization: Bearer fce_your_key" \
-H "Content-Type: application/json" \
-d '{"inbox":"test@ditapi.info"}'Step 3: Register a webhook
curl -X POST https://api2.freecustom.email/v1/webhooks \
-H "Authorization: Bearer fce_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/email-webhook",
"inbox": "test@ditapi.info"
}'Response:
{
"success": true,
"id": "wh_01jqz4abc123",
"inbox": "test@ditapi.info",
"url": "https://your-server.com/email-webhook"
}Save the id — you'll need it to delete the webhook later.
Step 4: Handle the incoming payload
When a new email arrives, we send a POST to your URL with:
{
"event": "new_message",
"inbox": "test@ditapi.info",
"message": {
"id": "msg_01jqz3k4m5n6p7q8",
"from": "noreply@github.com",
"subject": "Your GitHub verification code",
"date": "2026-04-03T09:55:00.000Z",
"has_attachment": false,
"otp": "482931",
"verification_link": "https://github.com/verify?token=abc123"
}
}Respond with any 2xx status to acknowledge receipt. We retry up to 3 times with exponential backoff on failure. After 10 consecutive failures, the webhook is automatically disabled.
Verifying Webhook Signatures
Every webhook request includes an X-FCE-Signature header: an HMAC-SHA256 signature of the raw request body, signed with your API key. Always verify this. It prevents attackers from sending fake webhook payloads to your endpoint.
Node.js / TypeScript
import express from "express";
import crypto from "crypto";
const app = express();
// Must parse as raw Buffer — not JSON — to verify the signature
app.use(express.raw({ type: "application/json" }));
function verifySignature(rawBody: Buffer, signature: string, apiKey: string): boolean {
const expected = crypto
.createHmac("sha256", apiKey)
.update(rawBody)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
app.post("/email-webhook", (req, res) => {
const sig = req.headers["x-fce-signature"] as string;
if (!verifySignature(req.body, sig, process.env.FCE_API_KEY!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const payload = JSON.parse(req.body.toString());
const { inbox, message } = payload;
console.log(`📬 New email in ${inbox}`);
console.log(` From: ${message.from}`);
console.log(` OTP: ${message.otp}`);
console.log(` Link: ${message.verification_link}`);
// Your logic here — write to DB, trigger test assertion, send Slack alert
res.sendStatus(200);
});
app.listen(3000);Python / FastAPI
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/email-webhook")
async def handle_webhook(request: Request):
body = await request.body()
sig = request.headers.get("x-fce-signature", "")
expected = hmac.new(
os.environ["FCE_API_KEY"].encode(),
body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = await request.json()
message = payload["message"]
print(f"📬 New email in {payload['inbox']}")
print(f" OTP: {message['otp']}")
print(f" Link: {message.get('verification_link')}")
return {"ok": True}Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
func verifySignature(body []byte, sig, apiKey string) bool {
mac := hmac.New(sha256.New, []byte(apiKey))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-FCE-Signature")
if !verifySignature(body, sig, os.Getenv("FCE_API_KEY")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var payload map[string]interface{}
json.Unmarshal(body, &payload)
msg := payload["message"].(map[string]interface{})
fmt.Printf("📬 New email in %s\n", payload["inbox"])
fmt.Printf(" OTP: %v\n", msg["otp"])
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/email-webhook", webhookHandler)
http.ListenAndServe(":8080", nil)
}PHP
<?php
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_FCE_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $_ENV['FCE_API_KEY']);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$payload = json_decode($body, true);
$message = $payload['message'];
error_log("📬 New email in {$payload['inbox']}");
error_log(" OTP: {$message['otp']}");
http_response_code(200);
echo json_encode(['ok' => true]);Workflow Automation: n8n, Zapier, Make
Webhooks are natively supported by every major workflow automation platform. Here's the pattern:
n8n:
Add a Webhook node as the trigger. Copy the generated URL.
Register it with FCE:
POST /v1/webhookswith{ "url": "...", "inbox": "..." }.Add downstream nodes — write the OTP to a Google Sheet, post it to Slack, trigger a test run.
Zapier:
Create a new Zap with Webhooks by Zapier → Catch Hook as the trigger.
Register the Zapier URL with FCE.
Map
message.otpormessage.verification_linkto downstream Zap steps.
Make (formerly Integromat): Same pattern. Use the Custom Webhook module as the trigger.
This lets non-engineers build email verification automations without writing a single line of code.
Managing Webhooks
List all webhooks
curl "https://api2.freecustom.email/v1/webhooks" \
-H "Authorization: Bearer fce_your_key"Delete a webhook
curl -X DELETE "https://api2.freecustom.email/v1/webhooks/wh_01jqz4abc123" \
-H "Authorization: Bearer fce_your_key"Checking failure counts
The list endpoint returns a failureCount for each webhook. If this creeps up, check that your endpoint is returning 2xx and that your signature verification is using the raw body (not the parsed JSON).
Testing Locally with ngrok
Your webhook endpoint must be publicly accessible. During development, use ngrok to expose your local server:
ngrok http 3000
# → https://abc123.ngrok.ioRegister the ngrok URL as your webhook endpoint, send a test email to your inbox, and watch the payload arrive at your local handler.
Reliability and Retry Behavior
We attempt delivery immediately when an email arrives
On failure (non-2xx or timeout), we retry up to 3 times with exponential backoff: 10s, 30s, 90s
After 10 consecutive delivery failures, the webhook is automatically disabled and must be re-registered
Your endpoint has a 10-second timeout — keep your handler fast and defer heavy processing to a queue
Frequently Asked Questions
What plan do I need for webhooks? Growth ($49/mo) or Enterprise ($149/mo). Webhooks require persistent server infrastructure on our side to fan out events. Lower plans can use long-polling (GET /v1/inboxes/{inbox}/wait) instead.
Can I register multiple webhooks for the same inbox? Yes, up to 5 webhooks per inbox on Growth and 20 on Enterprise. Each registered URL receives an independent copy of the event.
What happens if my server is down when an email arrives? We retry 3 times with exponential backoff. If all retries fail, the event is not queued — consider using our WebSocket endpoint for persistent bidirectional connections if your server has frequent downtime.
Does the webhook payload include the full email body? The webhook payload includes the email metadata, OTP, and verification link. For the full HTML and text body, use the message ID from the payload to call GET /v1/inboxes/{inbox}/messages/{id}.
Can I use webhooks without a public server? Not directly. Webhooks require a publicly routable HTTPS endpoint. If you don't have one, use long-polling or the MCP server instead — both work from localhost.
Is the payload signed? Yes. Every request includes X-FCE-Signature — an HMAC-SHA256 of the raw body signed with your API key. Always verify this before processing the payload.
Can I filter webhooks by event type? Currently, new_message is the only event type. Future event types (inbox quota warnings, domain expiry notifications) are on the roadmap.
How do I test webhook delivery? Use ngrok or Webhook.site during development. Register the public URL, send an email to your test inbox, and inspect the incoming payload.
Related Resources
Webhook API Reference — full endpoint documentation
Wait API (Long-Polling) — alternative to webhooks for scripts and agents
WebSocket Documentation — persistent bidirectional connections
MCP Server Guide — for AI agent integration
OTP Extraction API — extracting codes from emails
n8n Email Automation Guide — CI/CD integration patterns
API Pricing — plan comparison
Written by
Dishant Singh
A full stack developer with good knowledge of email server, SEO, proxies, and networking, have more than 3 years of experience in building webapps for the netizens. Developing open source, fast, and free SaaS for all.
Frequently Asked Questions
What plan do I need for webhooks?+
Growth ($49/mo) or Enterprise ($149/mo). Webhooks require persistent server infrastructure on our side to fan out events. Lower plans can use long-polling (GET /v1/inboxes/{inbox}/wait) instead.
Can I register multiple webhooks for the same inbox?+
Yes, up to 5 webhooks per inbox on Growth and 20 on Enterprise. Each registered URL receives an independent copy of the event.
What happens if my server is down when an email arrives?+
We retry 3 times with exponential backoff. If all retries fail, the event is not queued — consider using our WebSocket endpoint for persistent bidirectional connections if your server has frequent downtime.
Does the webhook payload include the full email body?+
The webhook payload includes the email metadata, OTP, and verification link. For the full HTML and text body, use the message ID from the payload to call GET /v1/inboxes/{inbox}/messages/{id}.
Can I use webhooks without a public server?+
Not directly. Webhooks require a publicly routable HTTPS endpoint. If you don't have one, use long-polling or the MCP server instead — both work from localhost.
Is the payload signed?+
Yes. Every request includes X-FCE-Signature — an HMAC-SHA256 of the raw body signed with your API key. Always verify this before processing the payload.
Can I filter webhooks by event type?+
Currently, new_message is the only event type. Future event types (inbox quota warnings, domain expiry notifications) are on the roadmap.
How do I test webhook delivery?+
Use ngrok or Webhook.site during development. Register the public URL, send an email to your test inbox, and inspect the incoming payload.
No comments yet. Be the first to share your thoughts.