Every developer who has ever automated an email verification flow knows the temptation: just set a setInterval at 2 seconds and keep calling GET /messages until something shows up. It works. It is also wasteful, brittle, and embarrassing to look at in code review.
The FreeCustom.Email Wait API (GET /v1/inboxes/{inbox}/wait) is the clean alternative. You make one HTTP request. Your connection stays open on our server. The moment a new email arrives in that inbox, we respond immediately with the message data. You get true near-real-time delivery using standard HTTP, no WebSocket upgrades, no polling loops, no wasted round trips.
This guide covers everything: how the Wait API works under the hood, when to use it, how to combine it with OTP extraction, and complete working examples in every major language.
How Long-Polling Works
Long-polling is an HTTP technique where the client sends a request and the server holds the connection open until new data is available — or a configurable timeout expires. The moment data arrives, the server responds and the client can immediately reconnect to wait for the next event.
It is the backbone of how Facebook Messenger worked before WebSockets were universally supported, and it is still the right choice when you want real-time delivery without the complexity of WebSocket protocol upgrades, persistent connection state management, or firewall issues.
The key advantages over short polling:
No wasted requests. With 2-second polling, you send 450 empty requests in 15 minutes waiting for an email. With long-polling, you send one.
Near-immediate delivery. The server responds the instant the email arrives, not at the next poll interval.
Standard HTTP. Works through proxies, firewalls, and CDNs that would block a WebSocket upgrade.
Simple to implement. No protocol negotiation, no connection state, no reconnect logic beyond a simple retry on timeout.
On our side, the Wait API uses Redis pub/sub. When an email arrives at the SMTP layer, we publish a message to a Redis channel keyed by inbox address. Every open /wait connection subscribed to that inbox receives the event immediately and returns the response.
Quick Start
# 1. Register an inbox (any plan)
curl -X POST https://api2.freecustom.email/v1/inboxes \
-H "Authorization: Bearer fce_your_key" \
-H "Content-Type: application/json" \
-d '{"inbox":"wait-test@ditapi.info"}'
# 2. Open a wait connection — blocks until email arrives (max 30s)
curl "https://api2.freecustom.email/v1/inboxes/wait-test@ditapi.info/wait?timeout=30" \
-H "Authorization: Bearer fce_your_key"
# 3. Once it returns, fetch the OTP
curl "https://api2.freecustom.email/v1/inboxes/wait-test@ditapi.info/otp" \
-H "Authorization: Bearer fce_your_key"The /wait endpoint responds with one of two shapes:
Email received:
{
"success": true,
"message": "New message received",
"data": {
"id": "msg_01jqz3k4m5n6",
"subject": "Your verification code",
"from": "noreply@acme.com"
}
}Timeout (no email within the window):
{
"success": false,
"message": "Timeout reached"
}API Reference
GET /v1/inboxes/{inbox}/wait
Plan requirement: Developer or above
Billing: 1 wait call consumes 10 monthly requests (reflects the server-side connection holding cost)
Query parameters:
Parameter | Type | Default | Description |
|---|---|---|---|
| integer | 30 | Max seconds to hold the connection open. Range: 10–60. |
| string | — | Optional. Last seen message ID. If a newer message already exists in the inbox, returns immediately instead of waiting. |
Headers: Authorization: Bearer fce_your_key
The since parameter is useful for avoiding a race condition where an email arrives between your last message fetch and your wait call. Pass the ID of the last message you saw and we will return immediately if a newer one exists, or hold the connection if not.
Node.js / TypeScript
const FCE_API = "https://api2.freecustom.email";
const KEY = process.env.FCE_API_KEY!;
async function waitForEmail(inbox: string, timeoutSec = 30): Promise<string | null> {
const res = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/wait?timeout=${timeoutSec}`,
{
headers: { Authorization: `Bearer ${KEY}` },
signal: AbortSignal.timeout((timeoutSec + 5) * 1000),
}
);
const data = await res.json();
if (!data.success) return null; // timeout
// Now fetch the OTP
const otpRes = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/otp`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const otpData = await otpRes.json();
return otpData.data?.otp ?? null;
}
// Usage
const inbox = "wait-test@ditapi.info";
const otp = await waitForEmail(inbox, 30);
if (otp) {
console.log(`✓ OTP received: ${otp}`);
} else {
console.log("✗ No email arrived within timeout");
}Python
import os, requests
FCE_API = "https://api2.freecustom.email"
HEADERS = {"Authorization": f"Bearer {os.environ['FCE_API_KEY']}"}
def wait_for_otp(inbox: str, timeout: int = 30) -> str | None:
# Step 1: hold connection open until email arrives
try:
wait_res = requests.get(
f"{FCE_API}/v1/inboxes/{inbox}/wait",
headers=HEADERS,
params={"timeout": timeout},
timeout=timeout + 5,
)
data = wait_res.json()
if not data.get("success"):
return None # timeout
except requests.exceptions.Timeout:
return None
# Step 2: fetch the OTP
otp_res = requests.get(f"{FCE_API}/v1/inboxes/{inbox}/otp", headers=HEADERS)
return otp_res.json().get("data", {}).get("otp")
# Usage
otp = wait_for_otp("wait-test@ditapi.info", timeout=30)
print(f"OTP: {otp}" if otp else "No email received")Go
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const fceAPI = "https://api2.freecustom.email"
type WaitResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type OtpData struct {
Otp string `json:"otp"`
}
type OtpResponse struct {
Success bool `json:"success"`
Data OtpData `json:"data"`
}
func waitForOtp(inbox string, timeoutSec int) (string, error) {
apiKey := os.Getenv("FCE_API_KEY")
client := &http.Client{Timeout: time.Duration(timeoutSec+5) * time.Second}
// Step 1: wait
waitURL := fmt.Sprintf("%s/v1/inboxes/%s/wait?timeout=%d", fceAPI, inbox, timeoutSec)
req, _ := http.NewRequest("GET", waitURL, nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var waitData WaitResponse
body, _ := io.ReadAll(resp.Body)
json.Unmarshal(body, &waitData)
if !waitData.Success {
return "", nil // timeout
}
// Step 2: get OTP
otpURL := fmt.Sprintf("%s/v1/inboxes/%s/otp", fceAPI, inbox)
otpReq, _ := http.NewRequest("GET", otpURL, nil)
otpReq.Header.Set("Authorization", "Bearer "+apiKey)
otpResp, err := client.Do(otpReq)
if err != nil {
return "", err
}
defer otpResp.Body.Close()
var otpData OtpResponse
otpBody, _ := io.ReadAll(otpResp.Body)
json.Unmarshal(otpBody, &otpData)
return otpData.Data.Otp, nil
}
func main() {
otp, err := waitForOtp("wait-test@ditapi.info", 30)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
if otp == "" {
fmt.Println("No email received within timeout")
return
}
fmt.Printf("OTP: %s\n", otp)
}Using since to Avoid Race Conditions
Consider this sequence of events:
You register an inbox
You trigger a signup flow that sends a verification email
The email arrives before you open the
/waitconnectionYour
/waitcall hangs until timeout because there is no new message
The since parameter solves this. Before triggering the signup, call GET /v1/inboxes/{inbox}/messages?limit=1 to get the ID of the most recent message (or an empty string if none). Pass that as ?since=msg_last_id to the wait endpoint. If a message newer than that ID already exists, we return it immediately.
async function waitForNewEmail(inbox: string, timeoutSec = 30): Promise<string | null> {
// Get current latest message ID (empty string if inbox is fresh)
const listRes = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/messages?limit=1`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const listData = await listRes.json();
const sinceId = listData.data?.messages?.[0]?.id ?? "";
// Now trigger your signup flow...
// Wait for a message newer than sinceId
const waitRes = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/wait?timeout=${timeoutSec}&since=${sinceId}`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const waitData = await waitRes.json();
if (!waitData.success) return null;
const otpRes = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/otp`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const otpData = await otpRes.json();
return otpData.data?.otp ?? null;
}Combining Wait API with Playwright
The Wait API is a perfect complement to Playwright E2E tests. Register the inbox, fill the form, open the wait connection, get the OTP, and continue the test:
typescript
import { test, expect } from "@playwright/test";
const FCE_API = "https://api2.freecustom.email";
const KEY = process.env.FCE_API_KEY!;
async function createInbox(): Promise<string> {
const inbox = `pw-${Date.now()}@ditapi.info`;
await fetch(`${FCE_API}/v1/inboxes`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ inbox }),
});
return inbox;
}
async function waitForOtp(inbox: string): Promise<string> {
// Hold the wait connection open while Playwright fills the form
const waitPromise = fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/wait?timeout=30`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
// Playwright work happens concurrently
// (the wait connection is open server-side, so timing doesn't matter)
const waitRes = await waitPromise;
const waitData = await waitRes.json();
if (!waitData.success) throw new Error("No email received");
const otpRes = await fetch(
`${FCE_API}/v1/inboxes/${encodeURIComponent(inbox)}/otp`,
{ headers: { Authorization: `Bearer ${KEY}` } }
);
const otpData = await otpRes.json();
return otpData.data.otp;
}
test("signup with email verification", async ({ page }) => {
const inbox = await createInbox();
// Open wait connection and fill form concurrently
const [, otp] = await Promise.all([
page.goto("https://your-app.com/signup").then(async () => {
await page.fill('[name="email"]', inbox);
await page.fill('[name="password"]', "Str0ng!Pass#99");
await page.click('[type="submit"]');
}),
waitForOtp(inbox),
]);
await page.fill('[name="otp"]', otp);
await page.click('[data-testid="verify-btn"]');
await expect(page).toHaveURL("/dashboard");
});See the complete Playwright & Selenium guide for parallel test configuration.
Wait API vs. Polling: The Numbers
At 2-second polling intervals, waiting for an email that arrives in 20 seconds means 10 API calls. Over a 30-test Playwright suite, that is 300 extra API calls — all returning empty responses, all consuming your monthly quota.
With the Wait API, each test makes 1 wait call and 1 OTP call. 30 tests = 60 calls total instead of 300+.
Approach | Calls per test (20s wait) | 30-test suite |
|---|---|---|
2-second polling | 10+ | 300+ |
5-second polling | 4+ | 120+ |
Wait API | 1 | 30 |
The Wait API costs 10 monthly requests per call (to reflect server-side connection cost), so the effective quota for a 30-test suite is 300 requests — the same total, but zero wasted round trips and genuinely immediate delivery.
Wait API vs. Webhooks vs. MCP — Decision Guide
Scenario | Best choice |
|---|---|
CLI script or one-off automation | Wait API |
Playwright/Selenium test | Wait API |
Persistent backend service | Webhooks |
n8n / Zapier workflow | Webhooks |
AI agent (Claude, GPT, LangChain) | MCP |
Browser-facing live inbox UI | WebSocket |
Low-volume script, free plan | Basic polling |
Frequently Asked Questions
What plan do I need for the Wait API? Developer ($7/mo) or above. The Wait API is not available on the Free plan because holding connections open requires server resources. The Free plan can use standard polling (GET /v1/inboxes/{inbox}/messages) instead.
How much does the Wait API cost? One /wait call consumes 10 monthly requests from your quota. This reflects the server-side cost of holding an HTTP connection open. Compared to polling every 2 seconds for 30 seconds (15 requests), it is the same or cheaper while delivering email immediately upon arrival.
What is the maximum timeout? 60 seconds. The minimum is 10 seconds. The default is 30. We enforce a hard cap because indefinite HTTP connections are not practical at scale — use our WebSocket endpoint (Startup+) for longer-lived connections.
Can I have multiple /wait connections open for the same inbox? Yes. Each connection is independent. This is useful for parallel test workers where multiple tests are waiting on different inboxes simultaneously.
Does /wait work behind a proxy or load balancer? Yes. The Wait API uses standard HTTP/1.1 keep-alive — no special protocol upgrade is required. You may need to set proxy_read_timeout to at least 65 seconds in nginx to avoid the proxy closing the connection before our 60-second cap.
What if the email arrives between my inbox registration and my wait call? Use the ?since= parameter. Pass the ID of the last message you saw (or an empty string if the inbox is fresh). If a newer message already exists, we return it immediately instead of waiting.
Is the Wait API available for custom domains? Yes. The inbox address just needs to be registered under your account. Custom domain inboxes work identically to platform domain inboxes.
Can I use the Wait API without registering the inbox first? No. The inbox must be registered via POST /v1/inboxes before you can wait on it. Attempting to wait on an unregistered inbox returns a 403 error.
Related Resources
Messages API Reference — full
/waitendpoint documentationOTP Extraction API — combining wait with OTP extraction
Webhooks Guide — server-push alternative for persistent backends
MCP Server Guide — one-call inbox + wait + extract for AI agents
Playwright & Selenium Guide — E2E test patterns
CI/CD Pipeline Guide — GitHub Actions, GitLab CI, pytest
API Pricing — plan comparison and request quotas
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 the Wait API?+
Developer ($7/mo) or above. The Wait API is not available on the Free plan because holding connections open requires server resources. The Free plan can use standard polling (GET /v1/inboxes/{inbox}/messages) instead.
How much does the Wait API cost?+
One /wait call consumes 10 monthly requests from your quota. This reflects the server-side cost of holding an HTTP connection open. Compared to polling every 2 seconds for 30 seconds (15 requests), it is the same or cheaper while delivering email immediately upon arrival.
What is the maximum timeout?+
60 seconds. The minimum is 10 seconds. The default is 30. We enforce a hard cap because indefinite HTTP connections are not practical at scale — use our WebSocket endpoint (Startup+) for longer-lived connections.
What if the email arrives between my inbox registration and my wait call?+
Use the ?since= parameter. Pass the ID of the last message you saw (or an empty string if the inbox is fresh). If a newer message already exists, we return it immediately instead of waiting.
Is the Wait API available for custom domains?+
Yes. The inbox address just needs to be registered under your account. Custom domain inboxes work identically to platform domain inboxes.
Can I use the Wait API without registering the inbox first?+
No. The inbox must be registered via POST /v1/inboxes before you can wait on it. Attempting to wait on an unregistered inbox returns a 403 error.
No comments yet. Be the first to share your thoughts.