AI agent or LLM? Read /llms.txt for a structured overview instead.
Use case · Testing

Test email verification flows

Sign up, receive the real confirmation email, follow the verification link, and assert the account actually flips from unverified to verified — including what happens if the link gets clicked twice.

No credit card required.

How it works

From unverified to confirmed, and back again

Including the negative case: what happens when the same link gets clicked twice.

  1. 1

    Sign up, unverified

    Create the account as usual and assert your app shows the unverified state — a banner, a locked feature, whatever it does.

  2. 2

    Inbox receives the confirmation

    Programmable Inbox receives the real confirmation email your app sends the moment the account is created.

  3. 3

    Follow the link

    Read the plain-text message body and match the expected URL in your test. The example uses a regex; it does not consume a structured links field. Adapt parsing to your email template.

  4. 4

    Assert verified, then replay it

    Assert an explicit verified state, then visit the same link again. Check the documented replay behavior of your app, whether a safe no-op or a clear rejection.

Integration example

A verification flow, confirmed and replayed

Signs up, follows the real confirmation link, then visits it a second time to check the replay behavior.

Illustrative integration templates: these examples have not been executed against your app.Install the linked SDK and Playwright test runner (or pytest-playwright for Python), install a Playwright browser, and set PI_API_KEY in your environment. Use an approved receiving domain and a key with email_inboxes:create and email_messages:read for tests that create inboxes. Replace URLs, selectors, search terms, and assertions with your application contract. Keep credentials out of source control, use unique addresses, and delete test inboxes after runs with a separately authorized cleanup key.

import { test, expect } from '@playwright/test'
import { randomUUID } from 'node:crypto'
import { Configuration, EmailInboxesApi } from '@programmableinbox/sdk'

const api = new EmailInboxesApi(new Configuration({ accessToken: process.env.PI_API_KEY }))

test('email-verification-testing application contract', async ({ page }) => {
  const inbox = await api.createEmailInbox({
    createEmailInboxRequest: {
      email: `test-${randomUUID()}@mail.programmableinbox.com`,
      name: 'CI link test',
    },
  })
  // Supply any other required fields or seed an account for your app.
  await page.goto('https://staging.yourapp.com/signup')
  await page.fill('#email', inbox.data.email)
  await page.click('button[type=submit]')

  // Retry for delivery, then parse the expected link from plain text.
  let body = ''
  await expect(async () => {
    const result = await api.getEmailInboxMessages({
      id: inbox.data.id, q: 'confirm your email', limit: 1,
    })
    body = result.data.messages[0]?.bodyText ?? ''
    expect(body).not.toBe('')
  }).toPass({ timeout: 20_000, intervals: [1000, 2000, 3000] })

  // Adapt the regex to your template and expected path. Not a general HTML parser.
  const link = body.match(/https:\/\/staging\.yourapp\.com\/[^\s<>"']+/)?.[0]
  if (!link) throw new Error('Expected application link missing from email')
  if (new URL(link).origin !== 'https://staging.yourapp.com') {
    throw new Error('Unexpected link origin')
  }

  await page.goto(link)
  await expect(page.locator('[data-testid=verified-status]')).toHaveText('Verified')
  // This template assumes replay is a safe no-op; adapt to your app's contract.
  await page.goto(link)
  await expect(page.locator('[data-testid=verified-status]')).toHaveText('Verified')
})

FAQ

Common questions

How is this different from testing a magic link?
A verification link confirms an address is real and flips an account flag — it usually doesn't sign you in on its own. A magic link is a passwordless sign-in mechanism. They often look similar in code, but they're testing different guarantees; see the magic-link testing page if that's what your flow actually does.
How do I test resending the verification email?
Record the first message ID, trigger your app's resend action, and retry until a different matching message arrives. A latest-message query alone can still return the old email while delivery is pending. Assert the new link works and test whether your app invalidates the original link.
What if my app requires verifying within a time limit?
Request the link as usual, then wait past your app's expiry window before visiting it, and assert your app rejects it with a clear error rather than silently doing nothing. Your app controls token expiry; email storage is separately subject to the inbox plan's retention limits.
Can I test signup with an already-registered email?
Yes — reuse the same inbox address across two signup attempts in the same test and assert your app's actual behavior, whether that's a clear error or a fresh verification email to the existing account.
Does this work in CI?
Yes. Creating the inbox and reading the message back are both plain API calls authenticated with a scoped key — nothing interactive that would break in a headless runner.

Spin up your secondary inbox

Create a programmable address, grab your first OTP, and wire up a rule in minutes. Self-host for free, or sign up for managed cloud — either way, you own your data.

No credit card required · AGPL v3 license · Community supported