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

Test passwordless magic-link logins

Request a magic link, receive the real email, pull the sign-in URL straight out, and follow it in a fresh browser context โ€” the way an actual user clicking from their phone would.

No credit card required.

How it works

From requested link to a real signed-in session

Followed the way an actual user would โ€” in a fresh browser context, not the session that requested it.

  1. 1

    Request the magic link

    Enter the inbox's address wherever your app asks for an email to sign in with โ€” no password field involved.

  2. 2

    Inbox receives it

    Programmable Inbox receives the real sign-in email your app sends โ€” not a mock, not an intercepted request.

  3. 3

    Extract 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

    Follow it & assert

    Open the link in a fresh browser context, the way a real user clicking from a phone or another device would, and assert you're signed in.

Integration example

A magic-link login, opened in a fresh context

Requests the link, extracts it, then opens it the way a user clicking from another device would.

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('magic-link-testing application contract', async ({ page, browser }) => {
  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/login')
  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: 'sign in', 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')
  }

  // A new page in the old context would still share its cookies.
  const freshContext = await browser.newContext()
  try {
    const signedInPage = await freshContext.newPage()
    await signedInPage.goto(link)
    await expect(signedInPage.locator('[data-testid=account-menu]')).toBeVisible()
  } finally {
    await freshContext.close()
  }
})

FAQ

Common questions

How is this different from testing an OTP flow?
An OTP is a short code you type back into a form your test is already on. A magic link is a URL you follow โ€” often from a different device or a fresh session than the one that requested it. If your flow uses a typed code instead, see the OTP testing page for that pattern.
Why open the link in a fresh browser context instead of the same page?
Following the link in the same Playwright page that requested it shares cookies with that session, which can mask bugs a real user would hit โ€” someone clicking a magic link from their phone has no existing session at all. A new browser context tests cross-session login, but it does not emulate another device. If your app intentionally binds links to the requesting session, assert that policy instead.
What if the link expires before I click it?
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 a broken page. Your app controls token expiry; email storage is separately subject to the inbox plan's retention limits.
Can I test what happens if the same link is clicked twice?
Yes โ€” visit the extracted URL once to sign in, then visit it again in another fresh context and assert your app either rejects it or handles the replay safely, depending on what your flow is supposed to do.
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