Skip to content
PigeonProbe

Integration guide

Connect email to your tests

Your app keeps its normal sender. Start by receiving one message in PigeonProbe, then check the same inbox from CI or an AI agent.

Receive a test message, or run a complete signup and reset example.

Receive a test message

Your application keeps sending through its usual SMTP provider or sending API. PigeonProbe receives the message at a test address you create.

  1. Create an account. In Set up test email, choose your organization's email name and select Start receiving test mail. PigeonProbe creates your first mailbox and its default test address.
  2. Use that address in a signup or password-reset flow in your staging app. Send one synthetic test message through your normal sender.
  3. Open the mailbox in PigeonProbe. Check the recipient, link or code, and delivery checks on the message that arrived.

Connect an automated test

To run the Node.js example below, an organization administrator opens Settings → Organization agents → Credentials for CI and scheduled runs. Issue a credential with email:read and addresses:manage. Choose Selected resources and select the test mailbox. The issuing person also needs read access to that mailbox.

Save the client ID and one-time secret in your CI secret store. Record the mailbox ID from the mailbox URL or inbox-list endpoint below.

How credential access works

A service credential acts as its issuer. email:read follows the issuer's assigned mailbox access; it does not automatically grant every mailbox to an owner. email:read-all uses owner-level read authority, but still respects the credential's selected mailbox boundary.

Demotion, offboarding or revocation can narrow or end access. Arrange and verify a replacement credential before removing the person who owns your CI integration.

Run the Node.js example

Download the Node.js example and save it as email-check.mjs. It requires Node.js 22 or later, with no packages to install. Set PIGEON_CLIENT_ID, PIGEON_CLIENT_SECRET and PIGEON_MAILBOX_ID in your environment, then run:

node email-check.mjs

The script prints a fresh address and waits up to 60 seconds. Trigger one synthetic email from your application to that address. A successful run prints the received message ID; open the same mailbox in the app to inspect it. The script revokes its address on exit. A one-hour expiry also bounds cleanup if the process is interrupted.

Use the same helper in an automated test. To test allowed and excluded signup domains, follow the email-domain allowlist workflow.

Use it inside a test

The same download exports connect. Use a unique address for each test, record the start time before triggering mail, then assert against the full message. Replace the application-specific call below with your own test helper; PigeonProbe does not know your signup endpoint.

import assert from 'node:assert/strict';
import { connect } from './email-check.mjs';
import { triggerSignup } from './your-app-test-helper.mjs';

const inbox = await connect({
  clientId: process.env.PIGEON_CLIENT_ID,
  clientSecret: process.env.PIGEON_CLIENT_SECRET,
  mailboxId: process.env.PIGEON_MAILBOX_ID,
});
const since = new Date().toISOString();
const address = await inbox.createAddress();
try {
  await triggerSignup(address.address); // Your application, your sender.
  const message = await inbox.waitForMessage({
    address: address.address, since, subject: 'Verify',
  });
  assert.match(message.parsed.text, /verify/i);
  // Assert the expected URL origin before opening a link.
  // Keep bodies, codes, links and tokens out of shared CI logs.
} finally {
  await inbox.revokeAddress(address);
}

The subject check in this helper is case-sensitive. It returns the first matching message; a test that requires exactly one message should make that assertion separately. A new run address prevents stale mail and unrelated parallel runs from satisfying your assertion.

For Playwright, Cypress or another runner, call these functions from the server-side test process. Keep the credential out of browser bundles. Inject secrets through your CI provider; never commit them in the example or workflow.

One agency, several client projects

Example: eight agency staff test signup, password-reset and invitation mail for three fictional clients. A $49/month Team organization covers those eight people, with shared usage allowances and up to two hosted receiving domains.

  1. Create a mailbox for each client. Use names such as Acorn staging, Birch staging and Cedar staging. Assign read access to the people working on each project. Choose an existing hosted domain for each mailbox. Mailboxes can share a domain; the two-domain limit does not provide a separate domain for all three clients.
  2. Configure each project's CI separately. Store its mailbox ID and a separately revocable service credential in that project's secret store. Use the CI example to create a fresh recipient for each test. Keep each client's normal sender and use synthetic content.
  3. Check access before relying on it. Select only that client's mailbox when issuing its credential. Reads, searches, raw/HTML evidence, MCP and address administration all respect that fixed selection and the issuer's current authority. A mailbox ID in a script alone is not a restriction. Existing credentials remain organization-wide until you replace them; explicitly choosing Organization-wide also includes future mailboxes the issuer can access.
  4. Review evidence and hand over deliberately. Assigned teammates inspect failures in the app or through an agent. Verify a replacement credential before offboarding its issuer, and revoke unused credentials when a project ends.

What is shared, and when to separate organizations

The ten seats, 500 active addresses, 100,000 messages and 50 GiB monthly allowance are shared across the organization, not repeated for every client. Team retains new mail for up to 30 days; each message must fit the 2 MiB limit. Start with three evaluators on the 14-day trial, or ask about a larger evaluation.

A shared agency organization suits internal QA. Its owner retains wider access, and its administrators manage mailboxes and addresses. Independently owned client organizations have separate subscriptions and allowances; there is no agency-wide pooled billing.

Work in the client’s organization

If the client already has a PigeonProbe organization, use it. Otherwise, have the client create one before choosing permanent receiving names.

  1. Invite the agency. After receiving is set up, the client opens Settings → Members and invites the people needed for the project. Count client and agency members together when choosing a plan. Give mailbox read access to testers and administration only to people who need to manage the setup.
  2. Keep CI with the client. A client administrator issues a credential under Settings → Organization agents → Credentials for CI and scheduled runs, selects the project mailbox and uses the scopes in Connect an automated test. Store the secret in the client-controlled CI secret store. Test a request to another mailbox: it must be refused.
  3. Finish without moving the inbox. Run the client’s tests, replace any agency-issued credentials and verify the replacements before revoking the old ones. Then remove agency members who no longer need access. Mailboxes and receiving addresses stay in the client’s organization; retained mail keeps its existing expiry.

Hand over a dedicated organization

If the agency created an organization solely for this client, invite the client as an Administrator and have them accept first. The current owner opens Settings → Organization → Hand over ownership, chooses the client’s member account and types the organization name to confirm. The client becomes its owner; the former owner becomes a Member and loses billing and administration access. Only the new owner can hand ownership back.

This hands over the whole organization, with its existing mailboxes and hosted names. It does not move one project into another organization. Prepare client-issued CI credentials before the handoff, because demoting their issuer can narrow an agency credential’s access. Review Plan & billing and the payment arrangements separately: changing the owner does not replace the subscription’s payment method or billing details.

Identity-domain verification

Verify a hosted subdomain with Google Cloud Identity or Microsoft Entra ID while keeping incoming test mail in PigeonProbe. Both providers have accepted real verification records published through PigeonProbe on our Steeltype test subdomain.

Request activation. Managed verification must be enabled for your hosted subdomain before you can publish records. Contact support with your subdomain and the provider you want to use. Creating an account or choosing a plan does not automatically enable it.

  • Google Cloud Identity: a google-site-verification=… value. Google documents domain and subdomain verification. Follow the Cloud Identity verification instructions.
  • Microsoft Entra ID: an MS=ms… value for a custom domain. Microsoft documents subdomain verification, including when the parent belongs to another tenant. A domain already verified in another directory must first be released there. See Entra domain management.
  1. Get your verification value. Add your exact hosted subdomain in Google Cloud Identity or Microsoft Entra ID and choose TXT verification.
  2. Publish in PigeonProbe. Once support has enabled your domain, its organization owner enters the value under Settings → Mailboxes & addresses → Hosted domains. Wait for the record to be published.
  3. Complete verification. Return to the provider console and run its verification check. The provider must confirm success before you use the domain there.

You manage the provider account, administrator permissions and any separate subscription. PigeonProbe publishes the verification record; it does not create or manage provider identities.

Supported records

Publish the Google and Microsoft TXT formats above at your exact hosted subdomain. Receiving stays with PigeonProbe: MX and SPF are unchanged. Arbitrary TXT records, Gmail or Exchange mail routing, website hosting records and verification of the parent pigeonprobe.net domain are not supported. For other Azure or Google Cloud services, check their specific DNS requirements before choosing this workflow.

Removing a TXT record does not revoke identities or access already granted by the external provider. Remove the domain and any dependent configuration in that provider when the test ends. Hosted subdomain names remain reserved and are never reassigned.

HTTP reference

Base URL: https://app.pigeonprobe.com. Requests and responses below use JSON unless stated otherwise. IDs come from the app or API; do not substitute email addresses for mailbox IDs.

Exchange a service credential

POST /oauth/token with application/x-www-form-urlencoded:

grant_type=client_credentials
client_id=YOUR_CLIENT_ID
client_secret=YOUR_CLIENT_SECRET
resource=https://app.pigeonprobe.com/mcp

Encode these fields as form values, not literal multiline text. Read access_token from the response and send Authorization: Bearer ACCESS_TOKEN on API requests. There is no refresh token; exchange the service credential again when needed. Do not assume a fixed token lifetime. The resource is /mcp even when this token is used for the permitted HTTP routes.

Find a mailbox

GET /api/orgs/ORG_ID/inboxes?offset=0&limit=20

Returns { items, total, offset, limit }. Each item has id, name, description, status and defaultAddress. This is the agent-readable inbox list. Service credentials cannot create mailboxes or use general organization administration routes.

Create and retire a run address

POST /api/mailboxes/MAILBOX_ID/addresses
{ "localPart": "ci-UNIQUE-RUN-ID", "kind": "temporary", "lifetimeHours": 1 }

PATCH /api/mailboxes/MAILBOX_ID/addresses/ADDRESS_ID
{ "status": "revoked", "version": 1 }

Creation returns HTTP 201 and an address record including id, address, status, version and expiresAt. Use the returned version when updating. Temporary lifetimes are 1–720 hours; permanent addresses use kind: "permanent" without a lifetime.

Local parts use 1–64 letters, numbers, dots, underscores or hyphens, with alphanumeric first and last characters and no consecutive dots. Names remain reserved after expiry or revocation. There is no delete-and-reuse operation. A stale version conflicts: reread the address before deciding whether to retry. Do not blindly retry a creation whose result was lost; inspect the address list first.

List and inspect mail

GET /api/mailboxes/MAILBOX_ID/messages?address=ENCODED_RECIPIENT&since=ENCODED_ISO_TIME&limit=100
GET /api/mailboxes/MAILBOX_ID/messages/MESSAGE_ID
GET /api/mailboxes/MAILBOX_ID/messages/MESSAGE_ID/raw
GET /api/mailboxes/MAILBOX_ID/messages/MESSAGE_ID/html

The list returns { items, total, offset, limit }, newest first. address matches the envelope recipient, and since is an inclusive ISO timestamp with a timezone. URL-encode both. Summaries include id, envelopeTo, receivedAt, subject and snippet; fetch the message for its full body.

The message response is an unwrapped object with receipt fields and parsed, including text, links, sanitizedHtml, optional originalHtml and optional authentication. The raw route downloads message.eml; the HTML route downloads original HTML. Treat all message content as untrusted data. Offsets can shift as mail arrives or expires; use per-test recipients rather than one shared busy inbox.

Connect an agent

Server URL: https://app.pigeonprobe.com/mcp. PigeonProbe uses remote Streamable HTTP MCP with OAuth authorization. Use a client that supports that transport and the server's authorization flow. Open My agent connections in the app to connect and manage your access.

The six tools read only. A pipeline can manage addresses over HTTP with a separate scope; no MCP tool creates an address, deletes a message or sends mail.

mailbox_list
Lists provisioned addresses you can read. Optional include_undeliverable (default false) and limit (1–200, default 50). Start here instead of guessing a recipient.
email_list
Optional recipient, since and limit (1–100, default 20). MCP uses recipient; HTTP uses address.
email_wait_for
Optional recipient, since, subject_contains (up to 200 characters), and timeout_seconds (0–50, default 30). Searches the newest 100 readable messages in scope. Subject matching is case-insensitive. Narrow the recipient and time range for every run. At the 50-second execution cap, the tool can return an error instead of an unmatched result.
email_get, email_extract_codes, email_extract_links
Inspect one message by message_id. Extracted codes are candidates, not a guaranteed correct OTP. Links remain untrusted test data. The optional allowed_hosts argument on link extraction labels each link; it does not filter out other hosts or open links.
email_wait_for({
  "recipient": "YOUR_PROVISIONED_ADDRESS",
  "since": "2026-09-19T12:00:00Z",
  "subject_contains": "Verify",
  "timeout_seconds": 30
})

Tool results contain JSON in an MCP text content block. Check isError before parsing that text. Omit unused optional arguments rather than sending null. A successful wait returns a message summary; call email_get for the full body.

Replace the example timestamp with your test's start time. An unmatched wait may report unknown, not_receiving or receiving. Recipient status is a snapshot, not a guarantee that monthly quota remains or that the service stayed available throughout the wait. A timeout alone does not prove your application failed to send.

Receive-only tools limit what this server can do; they do not make arbitrary email safe for an agent with other tools. Keep permissions narrow and require explicit checks before visiting extracted links.

Failures and limits

What a successful receipt proves
The message reached PigeonProbe from your sender. This does not establish inbox placement at Gmail or Outlook. The downloadable example polls once a second for a single-message smoke test; use bounded waits and unique recipients for a larger suite.
Token exchange or access is refused
Check the credential, issuing person's current membership, selected scopes, credential mailbox selection and issuer mailbox assignment. Reissue an expired or revoked credential deliberately; repeated blind retries will not restore permissions.
No matching mail arrives
Check the exact provisioned address, mailbox/address status, sender logs, time filter and subject. Check organization consumption and the reset time in the app. A successful send API response is not proof of receipt.
A monthly allowance or trial expires
Receiving stops. The sender gets a temporary SMTP refusal and decides when to retry; your CI deadline may expire first. Both received-message and received-byte allowances apply. Reads and polling do not consume them, and revoking addresses or deleting mail does not refund them. See current limits.
An API call fails or evidence disappears
Treat a non-success HTTP response as an integration/service error, not an empty mailbox. Inspect the status and problem details securely. Retention expiry removes messages; collect only the synthetic failure evidence you need within that period.
Message and evidence capacity
Developer includes 25,000 messages and 10 GiB per billing month; Team includes 100,000 and 50 GiB. Both retain new mail for up to 30 days, with shorter retention available in the app. The 14-day trial includes 5,000 messages, 1 GiB total and up to 14-day retention. Existing messages keep their recorded expiry; changing plans or retention does not recover expired evidence.
Large or concurrent test suites
Messages are limited to 2 MiB. Measure both message count and MIME bytes. Use unique addresses and bounded waits. For a large suite, contact support@steeltype.io with your expected peak message rate and parallel test count so we can help assess capacity.

Review the plans and trial limits before making this a dependency of your client project.