Skip to main content

Log in to chatgpt.com from Cypress with a PIBX inbox

ChatGPT can sign you in with a one-time code sent to your email address. If that address is a ProgrammableInbox (PIBX) inbox, a Cypress test can read the code through the API instead of a human opening a mailbox. This guide uses the Testing SDK together with cy.origin and cy.session.

Read this first
  • The ChatGPT selectors below are best-effort. OpenAI changes the login pages often and we have not pinned them to a version. The PIBX half (inbox, code retrieval) is stable and covered by the package's tests; the page half you will likely need to adjust. Inspect the live page and fix the label/name selectors in step 4.
  • Expect bot detection. chatgpt.com sits behind Cloudflare. Cypress runs a real browser, but a challenge page can still appear, especially headless, from datacenter IPs or after repeated logins. That is outside PIBX's control. Cache the session (step 6) so you log in rarely, and see When it doesn't work.
  • Check OpenAI's terms before automating logins, and use a dedicated test account, never a personal one. This guide is for testing your own integration with an account you own.

How it fits together​

Cypress spec ──cy.pibxOtp──▶ Node process ──HTTPS──▶ PIBX GET /api/v1/emailInbox/{id}/otp
│ (holds sk_live_ key)
└─cy.origin──▶ chatgpt.com ─ emails a code ─▶ your PIBX inbox

The API key lives only in the Cypress Node process. pibxTasks removes PIBX_API_KEY from config.env as it registers, so Cypress.env() in a spec cannot read it and it never reaches the Settings panel, the command log or videos. (Call it from setupNodeEvents and return the same config, as in step 3.)

cy.pibxOtp reads the PIBX server's clock, runs the action you give it (the click that makes ChatGPT send the email), then waits only for a code that arrived after that moment. A code left in the inbox by a previous run is never returned.

Prerequisites​

  1. A PIBX API key (sk_live_…, created in the dashboard under API Keys) with scopes:
    • email_messages:read: required, reads the extracted code
    • email_inboxes:create: only for the one-time setup in step 1
  2. A PIBX inbox on a domain your deployment allows (EMAIL_INBOX_ALLOWED_DOMAINS).
  3. A ChatGPT account registered with that inbox's address.
  4. Cypress 13 or later and Node 18+.
npm i -D @programmableinbox/testing

1. Create the inbox (once)​

A ChatGPT account is identified by its email address, so the inbox is the account's identity. Create it once and reuse it for every run, not one per test.

warning

Addresses are permanent. A PIBX inbox address can never be reclaimed after deletion, so don't create-and-delete inboxes in a loop. Also avoid brand terms in the local part: openai, chatgpt and many other brands are rejected by the impersonation blocklist (422).

curl -X POST "$PIBX_BASE_URL/api/v1/emailInbox" \
-H "Authorization: Bearer $PIBX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "name": "QA login"}'

The response is { "data": { "id": "…", "email": "…" } }. Keep the id.

Then register the ChatGPT account with that address by hand, completing the verification email yourself (read it in the PIBX dashboard or with GET /api/v1/emailInbox/{id}/messages).

2. Environment​

Put these in cypress.env.json (git-ignored) or your CI secret store:

{
"PIBX_API_KEY": "sk_live_...",
"PIBX_INBOX_ID": "<inbox id from step 1>",
"CHATGPT_EMAIL": "[email protected]"
}

Add cypress.env.json to .gitignore. In CI, export the same names as CYPRESS_PIBX_API_KEY etc. Cypress strips the CYPRESS_ prefix.

Optional: PIBX_BASE_URL for a self-hosted instance, and PIBX_POLL_INTERVAL_MS (default 5000) to change how often the inbox is checked.

3. Register the package​

cypress.config.ts:

import { defineConfig } from 'cypress'
import { pibxTasks } from '@programmableinbox/testing/cypress'

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
pibxTasks(on, config)
return config
},
},
})

cypress/support/e2e.ts:

import '@programmableinbox/testing/cypress/support'
import './commands'

That is the whole integration. The support import adds cy.pibxOtp() and friends, with types.

4. Add a login command​

cypress/support/commands.ts:

declare global {
namespace Cypress {
interface Chainable {
/** Signs in to chatgpt.com using a one-time code from the PIBX inbox. */
chatgptLogin(email: string): Chainable<void>
}
}
}

Cypress.Commands.add('chatgptLogin', (email: string) => {
cy.session(
['chatgpt', email],
() => {
cy.origin('https://chatgpt.com', { args: { email } }, ({ email }) => {
cy.visit('/auth/login')
// Selectors: verify against the live page (see "Read this first").
cy.contains('button', /log in/i).click()
cy.get('input[type="email"], input[name="email"]').type(email)
})

cy.pibxOtp({
from: 'openai.com',
timeoutMs: 90_000,
// The click that makes ChatGPT send the code. Server time is read first.
after: () => {
cy.origin('https://chatgpt.com', () => {
cy.contains('button', /continue/i).click()
})
},
}).then((code) => {
cy.origin('https://chatgpt.com', { args: { code } }, ({ code }) => {
cy.get('input[autocomplete="one-time-code"], input[name="code"]').type(code)
cy.contains('button', /continue|verify/i).click()
})
})
},
{
validate() {
// Cheap proof the session is still good.
cy.request({ url: 'https://chatgpt.com/api/auth/session', failOnStatusCode: false })
.its('body.user')
.should('exist')
},
cacheAcrossSpecs: true,
},
)
})

export {}

Why it is shaped this way:

  • cy.session() is the outer call. It cannot be called inside cy.origin(); put cy.origin() inside the session setup instead.
  • Values cross into cy.origin() only through args, which must be structured-cloneable (strings are fine). Nothing from the surrounding scope is visible inside the callback.
  • cy.pibxOtp sits between cy.origin blocks because it talks to PIBX from the Node side, where the key lives. after may contain Cypress commands (including cy.origin) but not promises.

5. Use it in a spec​

describe('chatgpt.com', () => {
it('logs in with an emailed code', () => {
cy.chatgptLogin(Cypress.env('CHATGPT_EMAIL'))

cy.visit('https://chatgpt.com')
cy.url().should('not.include', '/auth')
})
})

Run it:

npx cypress run --spec cypress/e2e/chatgpt-login.cy.ts
# or interactively
npx cypress open

6. Session caching and rotation​

cy.session() caches cookies per ['chatgpt', email], so only the first test in a run (or the first after the session expires) does the emailed-code dance. That is what keeps you below Cloudflare's patience and your PIBX request volume. Use validate() to detect expiry rather than catching failures downstream.

The default poll interval is 5 seconds, about 12 requests for a 60-second wait. Raise it (PIBX_POLL_INTERVAL_MS, or pollIntervalMs on the command) rather than lowering it: the API may throttle a key that polls aggressively.

Run specs that log in by emailed code serially. Two tests waiting on the same inbox can take each other's code.

When it doesn't work​

SymptomLikely causeWhat to do
PibxAuthError (401)Wrong or revoked keyCheck PIBX_API_KEY; the key is shown once at creation.
PibxAuthError (403)Key lacks email_messages:readCreate a key with that scope.
PIBX API key is missing / No inbox idVariable not reaching CypressUse cypress.env.json or CYPRESS_-prefixed CI variables.
PibxHttpError … says the inbox was not foundWrong PIBX_INBOX_ID, or a key from a different organizationCopy the inbox id again; check the key and the inbox are in the same org. This fails immediately, before your action runs.
PibxTimeoutError, last status 404: No message with a one-time code…Mail never arrived, or from too narrowOpen the PIBX dashboard; check the message's From and loosen from.
PibxTimeoutError, last status 200: the newest code … is older than sinceAn old code is still in the inbox and no new mail arrivedSame as above: the action may not have sent an email (selector, bot challenge).
PibxTimeoutError with last error: …PIBX unreachableCheck PIBX_BASE_URL and the network from the Cypress machine.
Cypress sees a "Just a moment…" challenge pageCloudflare bot checkRun headed, use a residential/office IP, rely on cy.session caching. Do not try to defeat the challenge from the test.
cy.origin errors about session or importWrong nestingcy.session outside, cy.origin inside, only args crossing in.
Selector not foundThe ChatGPT page changedUpdate step 4's selectors against the live DOM.
  • Testing SDK: the package reference, including Playwright and plain-Node usage.
  • MCP Setup: the same one-time-code lookup via MCP, for agent-driven flows.
  • Authentication & Scopes: creating a key with the right scopes.