Webhooks
Subscribe to real-time HTTP POST notifications when new emails arrive in your registered inboxes. Webhooks require the Growth plan or above.
Cost per delivery: 2 requests (Basic), or 4 requests (OTP Present & Parsed).
POST /v1/webhooks
Register a webhook URL for a specific inbox. The URL must be publicly reachable over HTTPS.
curl -X POST https://api2.freecustom.email/v1/webhooks \
-H "Authorization: Bearer fce_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/callback",
"inbox": "target@ditapi.info"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string (uri) | ✓ | HTTPS endpoint to receive POST requests. |
| inbox | string (email) | ✓ | A registered inbox under your account. |
Responses
201Webhook registered
{
"success": true,
"id": "wh_01jqz4abc123",
"inbox": "target@ditapi.info",
"url": "https://your-server.com/callback",
"secret": "3f9a…c21e"
}Store secret: it is only returned once and is sent back in the X-Webhook-Secret header of every delivery.
403Plan too low or inbox not owned
{
"success": false,
"error": "plan_required",
"message": "Webhooks require the Growth plan ($89/mo) or above.",
"upgrade_url": "https://freecustom.email/api/pricing"
}GET /v1/webhooks
Returns all active webhook subscriptions for your account.
curl "https://api2.freecustom.email/v1/webhooks" \
-H "Authorization: Bearer fce_your_api_key"200Success
{
"success": true,
"count": 2,
"data": [
{
"_id": "wh_01jqz4abc123",
"inbox": "target@ditapi.info",
"url": "https://your-server.com/callback",
"createdAt": "2026-03-04T10:00:00.000Z",
"failureCount": 0
}
]
}DELETE /v1/webhooks/{id}
Unregisters a webhook by its ID. The ID is returned when you register a webhook.
curl -X DELETE "https://api2.freecustom.email/v1/webhooks/wh_01jqz4abc123" \
-H "Authorization: Bearer fce_your_api_key"200Deleted
{
"success": true,
"message": "Webhook wh_01jqz4abc123 unregistered."
}404Not found
{
"success": false,
"error": "not_found",
"message": "Webhook wh_01jqz4abc123 not found."
}Webhook Payload
When a new email arrives in a subscribed inbox, we send a POST request to your URL with the following JSON body:
{
"type": "new_mail",
"mailbox": "target@ditapi.info",
"plan": "pro",
"id": "msg_01jqz3k4m5n6p7q8",
"from": "noreply@github.com",
"to": "target@ditapi.info",
"subject": "Your GitHub verification code",
"date": "2026-03-04T09:55:00.000Z",
"hasAttachment": false,
"otp": "482931",
"verificationLink": "https://github.com/verify?token=abc123"
}The payload is a message summary: it does not include text, html or attachments. Fetch those with GET /v1/inboxes/{inbox}/messages/{id}. otp and verificationLink are null when nothing was detected.
Respond with any 2xx status within 10 seconds to acknowledge receipt. Each email is delivered once (no retries). After 10 consecutive failed deliveries the webhook is automatically disabled.
Security
Every webhook request includes an X-Webhook-Secret header containing the secret returned when you registered the webhook. Compare it to your stored value to make sure the request came from FreeCustom.Email:
import crypto from "crypto";
function verifyWebhook(headerSecret: string, storedSecret: string) {
const a = Buffer.from(headerSecret || "");
const b = Buffer.from(storedSecret);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Integration Examples
Node.js / Express
import express from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
app.post("/callback", (req, res) => {
const secret = req.headers["x-webhook-secret"] as string;
if (!verifyWebhook(secret, process.env.FCE_WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid secret");
}
const { mailbox, subject, otp } = req.body;
console.log(`New email in ${mailbox}: ${subject} (OTP = ${otp})`);
res.sendStatus(200);
});Python / FastAPI
import hmac, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/callback")
async def webhook(request: Request):
secret = request.headers.get("x-webhook-secret", "")
if not hmac.compare_digest(secret, os.environ["FCE_WEBHOOK_SECRET"]):
raise HTTPException(status_code=401, detail="Invalid secret")
payload = await request.json()
print(f"OTP: {payload['otp']}")
return {"ok": True}