The Wait API: Long-Poll for Temporary Email Without Polling Loops (2026 Guide)

Dishant SinghApril 6, 2026

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

timeout

integer

30

Max seconds to hold the connection open. Range: 10–60.

since

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:

  1. You register an inbox

  2. You trigger a signup flow that sends a verification email

  3. The email arrives before you open the /wait connection

  4. Your /wait call 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 create_and_wait_for_otp

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.


Written by

D

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.

FAQ

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.

Discussion0

No comments yet. Be the first to share your thoughts.