Magic Link Testing API in 2026: Automate Passwordless Login with FreeCustom.Email

Introduction

One-time passwords are everywhere in 2026: six-digit codes sent by email to verify logins, time-limited tokens gating account activation, numeric codes for passwordless signup. The fundamental promise of OTP authentication — expires fast, works only once — is exactly what makes testing it hard.

Manual OTP testing is tedious: trigger the email, open an inbox, copy the code, paste it into the form, click submit — all within the expiry window. At scale across 50 user scenarios in staging, this consumes hours.

Automating OTP flows requires infrastructure that:

  1. Creates a programmable test inbox

  2. Triggers the OTP email from your app

  3. Receives the email reliably and immediately

  4. Extracts the numeric code without manual parsing

  5. Uses it in the test flow before expiry

  6. Scales across parallel test workers

FreeCustom.Email is purpose-built for this. The official JavaScript and Python SDKs handle the entire lifecycle — register, trigger, wait, extract, cleanup — with typed APIs and no regex required.


Why General Email APIs Fail at OTP Testing

Problem

Impact

No OTP extraction

Regex in every test file; breaks when templates change

Client-side polling

Slow, burns API quota, fragile timing

Public inboxes

Another process can consume your OTP first

No timeout handling

Tests hang instead of failing fast

FreeCustom.Email solves all four with native extraction, server-side long-poll, private inboxes, and typed timeout exceptions.


Installation

JavaScript / TypeScript

npm install freecustom-email

Python

pip install freecustom-email

CLI (for shell scripting)

npm install -g fcemail

OTP Extraction: How It Works

On Growth+ plans, every incoming email is automatically scanned for:

  • Numeric OTP codes: 4–8 digit sequences in subject and body

  • Verification links: Complete URLs with authentication tokens

These appear as first-class fields on every message object:

{
  "id": "msg_abc123",
  "from": "noreply@yourapp.com",
  "subject": "Your verification code: 847291",
  "otp": "847291",
  "verification_link": null,
  "date": "2026-04-09T10:15:22.000Z"
}

On lower plans, otp returns "__DETECTED__" as a signal to upgrade. See OTP extraction documentation.


The most concise and reliable pattern — one call handles register, trigger, wait, and optional cleanup:

JavaScript / TypeScript

import { FreecustomEmailClient } from 'freecustom-email';

const client = new FreecustomEmailClient({
  apiKey: process.env.FCE_API_KEY!,
  timeout: 10_000,
  retry: { attempts: 2, initialDelayMs: 500 },
});

const otp = await client.getOtpForInbox(
  'signup-test@ditapi.info',
  async () => {
    // Your app sends the OTP email here
    await fetch('https://yourapp.com/api/send-otp', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'signup-test@ditapi.info' }),
    });
  },
  {
    timeoutMs: 30_000,
    autoUnregister: true,
  },
);

console.log('OTP:', otp); // '847291'

Python (async)

import asyncio, os, httpx
from freecustom_email import FreeCustomEmail

client = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])

async def send_otp_email():
    async with httpx.AsyncClient() as http:
        await http.post(
            "https://yourapp.com/api/send-otp",
            json={"email": "signup-test@ditapi.info"},
        )

async def main():
    otp = await client.get_otp_for_inbox(
        inbox="signup-test@ditapi.info",
        trigger_fn=send_otp_email,
        timeout_ms=30_000,
        auto_unregister=True,
    )
    print(f"OTP: {otp}")  # '847291'

asyncio.run(main())

Python (sync — no asyncio)

from freecustom_email import FreeCustomEmail
import requests

client = FreeCustomEmail(api_key="fce_...", sync=True)

client.inboxes.register("signup-test@ditapi.info")

# Trigger the OTP email
requests.post(
    "https://yourapp.com/api/send-otp",
    json={"email": "signup-test@ditapi.info"},
)

otp = client.otp.wait_for("signup-test@ditapi.info", timeout_ms=30_000)
print(f"OTP: {otp}")
client.inboxes.unregister("signup-test@ditapi.info")

Manual OTP Flow (Step by Step)

For cases where you need more control:

// JavaScript — step by step
await client.inboxes.register('mytest@ditapi.info');

// Trigger OTP send in your app
await fetch('https://yourapp.com/api/request-otp', {
  method: 'POST',
  body: JSON.stringify({ email: 'mytest@ditapi.info' }),
  headers: { 'Content-Type': 'application/json' },
});

// Wait for email with optional sender filter
const msg = await client.messages.waitFor('mytest@ditapi.info', {
  timeoutMs: 30_000,
  pollIntervalMs: 2_000,
  match: m => m.from.includes('noreply@yourapp.com'),
});
console.log('OTP:', msg.otp);
console.log('Link:', msg.verificationLink);

// Or use the dedicated OTP endpoint
const result = await client.otp.get('mytest@ditapi.info');
console.log('OTP:', result.otp);
console.log('Link:', result.verification_link);

await client.inboxes.unregister('mytest@ditapi.info');
# Python — step by step
await client.inboxes.register("mytest@ditapi.info")

# Trigger in your app
async with httpx.AsyncClient() as http:
    await http.post(
        "https://yourapp.com/api/request-otp",
        json={"email": "mytest@ditapi.info"},
    )

# Wait with filter
msg = await client.messages.wait_for(
    "mytest@ditapi.info",
    timeout_ms=30_000,
    poll_interval_ms=2_000,
    match=lambda m: "noreply" in m.from_,
)
print(f"OTP: {msg.otp}")

# Or dedicated endpoint
result = await client.otp.get("mytest@ditapi.info")
print(f"OTP: {result.otp}")
print(f"From: {result.from_}")

await client.inboxes.unregister("mytest@ditapi.info")

Pytest: Complete OTP Test Suite

# tests/test_otp_flow.py
import pytest, asyncio, os, time
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError, PlanError

@pytest.fixture(scope="session")
def client():
    return FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])

@pytest.fixture
async def otp_inbox(client):
    """Unique inbox per test, cleaned up after."""
    email = f"otp-test-{int(time.time())}@ditapi.info"
    await client.inboxes.register(email)
    yield email
    await client.inboxes.unregister(email)

@pytest.mark.asyncio
async def test_otp_is_6_digits(client, otp_inbox, app_api):
    """OTP is exactly 6 numeric digits."""
    await app_api.send_otp(otp_inbox)

    otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)

    assert otp.isdigit(), f"OTP contains non-numeric chars: {otp}"
    assert len(otp) == 6, f"Expected 6-digit OTP, got {len(otp)} digits: {otp}"

@pytest.mark.asyncio
async def test_otp_allows_verification(client, otp_inbox, app_api):
    """Valid OTP completes signup flow."""
    await app_api.signup(email=otp_inbox, password="SecurePass@2026!")

    otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)

    result = await app_api.verify_otp(email=otp_inbox, otp=otp)
    assert result.verified is True

@pytest.mark.asyncio
async def test_expired_otp_is_rejected(client, otp_inbox, app_api):
    """OTP rejected after expiry window."""
    await app_api.send_otp(otp_inbox)
    otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)

    # Wait past expiry (adjust to your app's test expiry setting)
    await asyncio.sleep(65)

    result = await app_api.verify_otp(email=otp_inbox, otp=otp)
    assert result.status == 422  # Expired

@pytest.mark.asyncio
async def test_otp_is_single_use(client, otp_inbox, app_api):
    """OTP cannot be used a second time."""
    await app_api.send_otp(otp_inbox)
    otp = await client.otp.wait_for(otp_inbox, timeout_ms=30_000)

    first = await app_api.verify_otp(email=otp_inbox, otp=otp)
    assert first.verified is True

    second = await app_api.verify_otp(email=otp_inbox, otp=otp)
    assert second.status == 422  # Already used

@pytest.mark.asyncio
async def test_otp_timeout_raises(client, otp_inbox):
    """WaitTimeoutError raised when no email arrives."""
    # Don't trigger any email — expect timeout
    with pytest.raises(WaitTimeoutError) as exc:
        await client.otp.wait_for(otp_inbox, timeout_ms=3_000)

    assert exc.value.inbox == otp_inbox
    assert exc.value.timeout_ms == 3_000

Jest: OTP Testing with Node.js

// tests/otp-flow.test.js
const { FreecustomEmailClient, TimeoutError, PlanError } = require('freecustom-email');

const client = new FreecustomEmailClient({
  apiKey: process.env.FCE_API_KEY,
  retry: { attempts: 2, initialDelayMs: 500 },
});

describe('OTP Verification Flow', () => {
  let testEmail;

  beforeEach(async () => {
    testEmail = `otp-jest-${Date.now()}@ditapi.info`;
    await client.inboxes.register(testEmail);
  });

  afterEach(async () => {
    await client.inboxes.unregister(testEmail);
  });

  test('OTP is 6 numeric digits', async () => {
    const otp = await client.getOtpForInbox(
      testEmail,
      () => fetch('https://staging.yourapp.com/api/send-otp', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: testEmail }),
      }),
      { timeoutMs: 30_000, autoUnregister: false }, // we handle cleanup in afterEach
    );

    expect(otp).toMatch(/^\d{6}$/);
  }, 40_000); // Jest timeout > wait timeout

  test('Valid OTP verifies account', async () => {
    const otp = await client.getOtpForInbox(
      testEmail,
      () => triggerSignup(testEmail),
      { timeoutMs: 30_000, autoUnregister: false },
    );

    const res = await fetch('https://staging.yourapp.com/api/verify-otp', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: testEmail, otp }),
    });
    expect(res.status).toBe(200);
  }, 40_000);

  test('TimeoutError when no email arrives', async () => {
    // No trigger — expect timeout
    await expect(
      client.otp.waitFor(testEmail, { timeoutMs: 3_000 })
    ).rejects.toBeInstanceOf(TimeoutError);
  }, 10_000);
});

Parallel OTP Testing

FreeCustom.Email's private inbox model is designed for parallel test workers:

// Test 3 services sending OTPs simultaneously
const emails = [
  `auth-${Date.now()}@ditapi.info`,
  `payment-${Date.now()}@ditapi.info`,
  `admin-${Date.now()}@ditapi.info`,
];

// Register all inboxes in parallel
await Promise.all(emails.map(e => client.inboxes.register(e)));

// Trigger OTP emails from each service simultaneously
await Promise.all([
  triggerAuthOtp(emails[0]),
  triggerPaymentOtp(emails[1]),
  triggerAdminOtp(emails[2]),
]);

// Wait for all OTPs in parallel — each isolated
const otps = await Promise.all(
  emails.map(e => client.otp.waitFor(e, { timeoutMs: 30_000 }))
);

console.log('Auth OTP:', otps[0]);
console.log('Payment OTP:', otps[1]);
console.log('Admin OTP:', otps[2]);

// Cleanup
await Promise.all(emails.map(e => client.inboxes.unregister(e)));

CLI: OTP in Shell Scripts

# Authenticate once
fce auth login

# One-liner OTP capture for shell scripts
OTP=$(fce otp test@ditapi.info)
echo "OTP: $OTP"

# CI step example
trigger_signup_email
OTP=$(fce otp $TEST_EMAIL)
curl -X POST https://staging.yourapp.com/api/verify \
  -d "email=$TEST_EMAIL&otp=$OTP"

See CLI documentation.


MCP: AI Agent OTP Testing (Growth+ plans)

{
  "mcpServers": {
    "fce-mcp": {
      "command": "npx",
      "args": ["-y", "fce-mcp-server"],
      "env": { "FCE_API_KEY": "your_growth_key" }
    }
  }
}

The create_and_wait_for_otp tool lets an AI agent test the entire OTP flow in a single call. See MCP documentation and AI agent use case.


Error Handling

import { TimeoutError, PlanError, RateLimitError, AuthError } from 'freecustom-email';

try {
  const otp = await client.otp.waitFor('test@ditapi.info', { timeoutMs: 30_000 });
} catch (err) {
  if (err instanceof TimeoutError) {
    throw new Error('OTP email not received — check email delivery pipeline');
  }
  if (err instanceof PlanError) {
    throw new Error(`OTP extraction requires Growth plan: ${err.message}`);
  }
  if (err instanceof RateLimitError) {
    await new Promise(r => setTimeout(r, err.retryAfter * 1000));
    // retry...
  }
  if (err instanceof AuthError) {
    throw new Error('Invalid FCE_API_KEY');
  }
}
from freecustom_email.errors import (
    WaitTimeoutError, PlanError, RateLimitError, AuthError
)

try:
    otp = await client.otp.wait_for("test@ditapi.info", timeout_ms=30_000)
except WaitTimeoutError as e:
    raise AssertionError(f"OTP not received in {e.timeout_ms}ms for {e.inbox}")
except PlanError as e:
    raise RuntimeError(f"Growth plan required: {e}. Upgrade at {e.upgrade_url}")
except RateLimitError as e:
    await asyncio.sleep(e.retry_after)
    otp = await client.otp.wait_for("test@ditapi.info", timeout_ms=30_000)

FAQ

Q: What OTP formats does FreeCustom.Email extract? 4–8 digit numeric codes and full verification URLs, including encoded tokens, JWTs, and UUIDs. See OTP documentation.

Q: What if the OTP is in an HTML button image? The extractor scans HTML and plaintext body for <a href> tags and text patterns. Image-embedded OTPs are not extracted. Use client.messages.get() for the full HTML body in that case.

Q: What plan is needed for OTP extraction? Growth ($49/mo). Developer and Startup plans return __DETECTED__ but not the actual code.

Q: Can I wait for OTP without the Growth plan? Yes — use client.messages.waitFor() to receive the full message, then parse the body yourself.

Q: How do I test OTP rate limiting? Send multiple OTP requests in rapid succession and assert the appropriate HTTP 429 response from your app.

Q: Is there a changelog? Yes — see the API changelog.



Magic Link Testing API in 2026: Automate Passwordless Login with FreeCustom.Email

Meta Description: Need to test magic links automatically? FreeCustom.Email extracts verification links from emails for automated Playwright and pytest magic link testing. Full JavaScript and Python SDK guide.


Introduction

Magic links — click-to-authenticate URLs sent by email — are the dominant pattern for passwordless login in 2026. They offer better security than passwords and better UX than OTP codes. But they create a specific testing challenge: your test must retrieve a URL embedded in an email, navigate to it, and assert authentication — all within the link's validity window.

Without the right infrastructure, magic link tests involve parsing <a href> tags from HTML bodies, URL-decoding tokens, and hoping the polling loop finds the email before the link expires.

FreeCustom.Email solves this natively: the verification_link field is automatically extracted from every email and available directly on the message object — no HTML parsing, no regex, no URL decoding required.


The verification_link field is extracted alongside otp on every message (Growth+ plans):

{
  "id": "msg_abc123",
  "from": "auth@yourapp.com",
  "subject": "Sign in to YourApp",
  "otp": null,
  "verification_link": "https://yourapp.com/auth/verify?token=eyJhbGci...abc123",
  "date": "2026-04-09T10:00:00.000Z"
}

The complete URL — including token query parameters, regardless of length or encoding — is returned ready to use.


Installation

# JavaScript
npm install freecustom-email

# Python
pip install freecustom-email

import { FreecustomEmailClient } from 'freecustom-email';

const client = new FreecustomEmailClient({
  apiKey: process.env.FCE_API_KEY!,
  retry: { attempts: 2, initialDelayMs: 500 },
});

async function testMagicLinkLogin(appUrl: string): Promise<string> {
  const email = `magic-${Date.now()}@ditapi.info`;

  // Register → trigger → wait → return link, all in sequence
  await client.inboxes.register(email);

  // Trigger your app to send the magic link
  await fetch(`${appUrl}/api/request-magic-link`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  });

  // Wait for email, match by subject if needed
  const msg = await client.messages.waitFor(email, {
    timeoutMs: 30_000,
    pollIntervalMs: 2_000,
    match: m => m.subject.toLowerCase().includes('sign in'),
  });

  if (!msg.verificationLink) {
    throw new Error('Magic link not found in email body');
  }

  await client.inboxes.unregister(email);
  return msg.verificationLink;
}

// Usage
const link = await testMagicLinkLogin('https://staging.yourapp.com');
console.log('Magic link:', link);
// https://staging.yourapp.com/auth/verify?token=eyJhbGci...

import asyncio, os, time, httpx
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError

client = FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])

async def get_magic_link(app_url: str) -> str:
    email = f"magic-{int(time.time())}@ditapi.info"

    await client.inboxes.register(email)

    async with httpx.AsyncClient() as http:
        await http.post(
            f"{app_url}/api/request-magic-link",
            json={"email": email},
        )

    msg = await client.messages.wait_for(
        email,
        timeout_ms=30_000,
        poll_interval_ms=2_000,
        match=lambda m: "sign in" in m.subject.lower(),
    )

    if not msg.verification_link:
        raise ValueError("No magic link found in email body")

    await client.inboxes.unregister(email)
    return msg.verification_link

async def main():
    link = await get_magic_link("https://staging.yourapp.com")
    print(f"Magic link: {link}")

asyncio.run(main())

// tests/magic-link.spec.ts
import { test, expect } from '@playwright/test';
import { FreecustomEmailClient, TimeoutError } from 'freecustom-email';

const client = new FreecustomEmailClient({
  apiKey: process.env.FCE_API_KEY!,
});

test.describe('Magic link authentication', () => {
  let email: string;

  test.beforeEach(async ({}, testInfo) => {
    email = `magic-w${testInfo.workerIndex}-${Date.now()}@ditapi.info`;
    await client.inboxes.register(email);
  });

  test.afterEach(async () => {
    await client.inboxes.unregister(email);
  });

  test('new user authenticates via magic link', async ({ page }) => {
    // Request magic link
    await page.goto('/auth/magic-link');
    await page.fill('[name="email"]', email);
    await page.click('[type="submit"]');
    await expect(page.locator('[data-testid="check-email"]')).toBeVisible();

    // Get link — no HTML parsing, no URL decoding
    const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });
    expect(msg.verificationLink).toBeTruthy();
    expect(msg.verificationLink).toContain('token=');

    // Navigate to magic link
    await page.goto(msg.verificationLink!);

    // Assert authenticated
    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
  });

  test('magic link is single-use', async ({ page }) => {
    await page.goto('/auth/magic-link');
    await page.fill('[name="email"]', email);
    await page.click('[type="submit"]');

    const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });
    const link = msg.verificationLink!;

    // First use — succeeds
    await page.goto(link);
    await expect(page).toHaveURL(/\/dashboard/);

    // Second use — fails
    await page.goto(link);
    await expect(page.locator('[data-testid="link-expired"]')).toBeVisible();
  });

  test('expired magic link shows appropriate error', async ({ page }) => {
    await page.goto('/auth/magic-link');
    await page.fill('[name="email"]', email);
    await page.click('[type="submit"]');

    const msg = await client.messages.waitFor(email, { timeoutMs: 30_000 });

    // Force expiry (if your app supports short test expiry)
    await page.goto(msg.verificationLink! + '&_force_expire=1');
    await expect(page.locator('[data-testid="link-expired"]')).toBeVisible();
    await expect(page.locator('[data-testid="request-new-link"]')).toBeVisible();
  });

  test('link not found email shows error state', async () => {
    // Don't trigger any email — expect timeout
    await expect(
      client.messages.waitFor(email, { timeoutMs: 3_000 })
    ).rejects.toBeInstanceOf(TimeoutError);
  });
});

# tests/test_magic_link.py
import pytest, asyncio, time
from freecustom_email import FreeCustomEmail
from freecustom_email.errors import WaitTimeoutError
from playwright.async_api import async_playwright

@pytest.fixture(scope="session")
def fce():
    return FreeCustomEmail(api_key=os.environ["FCE_API_KEY"])

@pytest.fixture
async def magic_email(fce):
    email = f"magic-{int(time.time())}@ditapi.info"
    await fce.inboxes.register(email)
    yield email
    await fce.inboxes.unregister(email)

@pytest.mark.asyncio
async def test_magic_link_authenticates(fce, magic_email):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        # Request magic link
        await page.goto("https://staging.yourapp.com/auth/magic-link")
        await page.fill('[name="email"]', magic_email)
        await page.click('[type="submit"]')

        # Wait for email and get link
        msg = await fce.messages.wait_for(magic_email, timeout_ms=30_000)
        assert msg.verification_link, "No magic link in email"
        assert "token=" in msg.verification_link

        # Use the link
        await page.goto(msg.verification_link)
        assert "/dashboard" in page.url

        await browser.close()

@pytest.mark.asyncio
async def test_no_email_raises_timeout(fce, magic_email):
    """WaitTimeoutError when no email arrives."""
    with pytest.raises(WaitTimeoutError) as exc:
        await fce.messages.wait_for(magic_email, timeout_ms=3_000)
    assert exc.value.inbox == magic_email

For apps with very short link expiry windows, use WebSocket for the fastest possible capture:

const ws = client.realtime({
  mailbox: 'magic-test@ditapi.info',
  autoReconnect: false,
});

await new Promise<void>((resolve, reject) => {
  const timer = setTimeout(() => {
    ws.disconnect();
    reject(new Error('Magic link not received within 30s'));
  }, 30_000);

  ws.on('email', async (email) => {
    clearTimeout(timer);
    ws.disconnect();
    if (!email.verificationLink) {
      reject(new Error('Email arrived but no magic link found'));
      return;
    }
    console.log('Magic link:', email.verificationLink);
    // Navigate browser to link...
    resolve();
  });

  ws.connect();
});

See WebSocket documentation.


{
  "mcpServers": {
    "fce-mcp": {
      "command": "npx",
      "args": ["-y", "fce-mcp-server"],
      "env": { "FCE_API_KEY": "your_growth_key" }
    }
  }
}

The create_and_wait_for_otp MCP tool returns both otp and verification_link — AI agents can handle full magic link flows without additional code. See MCP documentation.


FAQ

Q: What if the magic link is inside an HTML button? FreeCustom.Email scans <a href="..."> tags in both HTML and plaintext bodies. Links inside button elements are captured. The full message body is also available via client.messages.get() if needed.

Q: Do complex tokens (JWTs, UUIDs, encoded params) work? Yes. The verification_link field returns the complete raw URL — no decoding or truncation.

Q: How fast is email delivery? Typically 1–3 seconds from send to receipt. WebSocket delivers in under 200ms. Long-poll returns the moment the email arrives.

Q: What plan is needed for verification_link extraction? Growth ($49/mo). On lower plans, use client.messages.waitFor() and parse msg.html or msg.text manually.

Q: How do I test that the app re-sends a new link correctly? Call client.messages.waitFor() twice — once for the initial link, click resend, then wait again. The second call receives the new message.

Q: Is there a security testing guide? Yes — see the security and penetration testing use case.

Get started freenpm install freecustom-emailpip install freecustom-emailRead OTP extraction docsView all use cases

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

Q: What if the OTP is in an HTML button image?+

The extractor scans HTML and plaintext body for &lt;a href&gt; tags and text patterns. Image-embedded OTPs are not extracted. Use client.messages.get() for the full HTML body in that case.

Q: What plan is needed for OTP extraction?+

Growth ($49/mo). Developer and Startup plans return __DETECTED__ but not the actual code.

Q: Can I wait for OTP without the Growth plan?+

Yes — use client.messages.waitFor() to receive the full message, then parse the body yourself.

Q: How do I test OTP rate limiting?+

Send multiple OTP requests in rapid succession and assert the appropriate HTTP 429 response from your app.

Q: Is there a changelog?+

Yes — see the API changelog.

Q: What if the magic link is inside an HTML button?+

FreeCustom.Email scans &lt;a href="..."&gt; tags in both HTML and plaintext bodies. Links inside button elements are captured. The full message body is also available via client.messages.get() if needed.

Q: Do complex tokens (JWTs, UUIDs, encoded params) work?+

Yes. The verification_link field returns the complete raw URL — no decoding or truncation.

Q: How fast is email delivery?+

Typically 1–3 seconds from send to receipt. WebSocket delivers in under 200ms. Long-poll returns the moment the email arrives.

Q: How do I test that the app re-sends a new link correctly?+

Call client.messages.waitFor() twice — once for the initial link, click resend, then wait again. The second call receives the new message.

Q: Is there a security testing guide?+

Yes — see the security and penetration testing use case.

Discussion0

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