Webhooks

Subscribe to real-time HTTP POST notifications when new emails arrive in your registered inboxes. Webhooks require the Growth plan or above.

POST /v1/webhooks

Register a webhook URL for a specific inbox. The URL must be publicly reachable over HTTPS.

curl
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

FieldTypeRequiredDescription
urlstring (uri)HTTPS endpoint to receive POST requests.
inboxstring (email)A registered inbox under your account.

Responses

201Webhook registered

json
{
  "success": true,
  "id": "wh_01jqz4abc123",
  "inbox": "target@ditapi.info",
  "url": "https://your-server.com/callback"
}

403Plan too low or inbox not owned

json
{
  "success": false,
  "error": "plan_required",
  "message": "Webhooks require the Growth plan ($49/mo) or above.",
  "upgrade_url": "https://freecustom.email/api/pricing"
}

GET /v1/webhooks

Returns all active webhook subscriptions for your account.

curl
curl "https://api2.freecustom.email/v1/webhooks" \
  -H "Authorization: Bearer fce_your_api_key"

200Success

json
{
  "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
curl -X DELETE "https://api2.freecustom.email/v1/webhooks/wh_01jqz4abc123" \
  -H "Authorization: Bearer fce_your_api_key"

200Deleted

json
{
  "success": true,
  "message": "Webhook wh_01jqz4abc123 unregistered."
}

404Not found

json
{
  "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:

json
{
  "event": "new_message",
  "inbox": "target@ditapi.info",
  "message": {
    "id": "msg_01jqz3k4m5n6p7q8",
    "from": "noreply@github.com",
    "subject": "Your GitHub verification code",
    "date": "2026-03-04T09: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.

Security

All webhook requests include an X-FCE-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your API key. Verify it to ensure the request originated from FreeCustom.Email:

typescript
import crypto from "crypto";

function verifyWebhook(rawBody: string, signature: string, apiKey: string) {
  const expected = crypto
    .createHmac("sha256", apiKey)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex")
  );
}

Integration Examples

Node.js / Express

typescript
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/callback", (req, res) => {
  const sig = req.headers["x-fce-signature"] as string;
  const raw = req.body.toString();

  if (!verifyWebhook(raw, sig, process.env.FCE_API_KEY!)) {
    return res.status(401).send("Invalid signature");
  }

  const { event, inbox, message } = JSON.parse(raw);
  console.log(`New email in ${inbox}: OTP = ${message.otp}`);

  res.sendStatus(200);
});

Python / FastAPI

python
import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/callback")
async def 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()
    print(f"OTP: {payload['message']['otp']}")
    return {"ok": True}