Email Testing using API

Send through Sendpit, read the captured mail back over the REST API, and assert on it in your test suite.

Email Testing using API

Point your application's mailer at Sendpit, then read the captured messages back over the REST API to assert on them in your test suite. Nothing is ever delivered to a real recipient.

Just need SMTP? If you only want to capture mail in a browser inbox without automating assertions, the Integration Guide covers configuration for PHP, Node.js, Python, Ruby, Java and Go. This page is about the automated testing loop.


1. What You Need

Create a mailbox and collect one set of credentials from it, under that mailbox's Settings → SMTP credentials.

Purpose Where to find it
Sending mail from your app Settings → SMTP credentials
Reading mail back in tests The same credentials

The same username and password do both jobs: your app authenticates to SMTP with them, and your tests authenticate to the REST API with them over HTTP Basic. Because there is only one credential, the mailbox you send to is always the mailbox you read from.

Treat the password as you would any other secret. Regenerating it rotates SMTP and API access together.


2. Point Your App at Sendpit

Replace your application's outbound mail settings with the mailbox's SMTP credentials.

Setting Value
Host smtp.sendpit.com
Port 587 STARTTLS (recommended), 465 implicit TLS, or 2525 STARTTLS for networks that block the others
Username Your mailbox's SMTP username
Password Your mailbox's SMTP password
Encryption Required on every port

Port 25 is deliberately closed. Sendpit accepts authenticated submission only, so there is no anonymous route in.

Any Recipient Address Works

Sendpit routes mail by who authenticated, not by who it was addressed to. Your username determines the destination mailbox, so you can send to anything@example.test, to your users' real addresses, or to whatever address your test happens to generate. It all lands in your mailbox and reaches nobody.

This means you do not have to change any recipient logic in your application to make it testable. Change the transport and leave the addressing alone.

Keep Certificate Verification On

smtp.sendpit.com presents a publicly trusted certificate. If your mail library reports a certificate verification failure, treat it as a real finding rather than something to switch off. It usually indicates an intercepting proxy, an outdated CA bundle in your container image, or a hostname typo.


3. Read the Mail Back

Everything captured is available over HTTPS. Authenticate with the mailbox's SMTP credentials over HTTP Basic.

curl https://sendpit.com/api/v1/messages \
  -u "mb_a1b2c3d4e5f6:your-smtp-password" \
  -H "Accept: application/json"
Endpoint What it gives you
GET /messages Captured messages, newest first, paginated
POST /messages/wait Blocks until a matching message arrives. Use this in tests
GET /messages/{id} A single message
GET /messages/{id}/body/html The HTML body, with /body/text alongside it
GET /messages/{id}/headers Full header set
GET /messages/{id}/raw The original MIME source
GET /messages/{id}/extractions Links and one-time codes pulled out of the message, for confirmation flows
GET /messages/{id}/attachments Attachment list, each downloadable by index
GET /messages/{id}/inspection Spam analysis, size, hash and message-id
DELETE /messages Empties the mailbox. Call this between test runs

The {id} in those paths is the same id Sendpit reports when it accepts the message over SMTP (250 Message accepted id=...), so a test that captures it from the send can read the message straight back instead of polling for it.

The full reference, including request and response schemas for every endpoint, is at /api/docs.

Waiting For a Message

Mail delivery is asynchronous, so a test that asserts immediately after triggering an action will usually lose the race. POST /messages/wait holds the connection open until something matches, which removes arbitrary sleeps from your suite.

{
  "filters": {
    "to": "new-user@example.test",
    "subject": "Confirm your email"
  },
  "timeout": 15,
  "max_results": 1
}

The filters sit inside a filters object. A flat payload is accepted but matches nothing, which is indistinguishable from a timeout. timeout is in seconds, defaulting to 15 and capped at 30.

Status Meaning
200 Matched. The body carries the messages
204 The window closed with nothing matching. Your assertion fails here
409 Several messages matched while max_results was 1. Tighten the filter

Available filters: to, from, cc, subject, body, headers, has_attachments, unread, received_after, received_before, and match set to exact or contains.


4. The Shape of a Test

Four moves, in this order. The sequence matters: clearing first is what makes the run repeatable, and waiting before asserting is what makes it stable.

  1. Clear the mailbox. DELETE /messages. Leftovers from an earlier run are the usual cause of a test that passes alone and fails in a suite.
  2. Do the thing that sends mail. Register the user, trigger the password reset, place the order. Drive it through your application's real path rather than calling the mailer directly, so the test covers the wiring too.
  3. Wait for the message. POST /messages/wait, filtered to the recipient your test generated. Give each test a unique address and the filter becomes exact.
  4. Assert on what arrived. Subject, sender, recipients, body content, the presence of a confirmation link, an attachment count. Pull the link or one-time code out of /extractions and follow it to test the rest of the flow.

5. Plans & Access

Sending over SMTP works on every plan. API access requires Basic or higher. On the Free plan the API answers 403 plan_required to every request.

Plan API access Requests per minute
Free None
Basic Full 60
Pro Full 120
Max Full 300

A parallel test suite consumes request budget faster than you might expect, since each test spends at least one clear, one wait and one read. Check GET /rate-limit for your live headroom.


6. Troubleshooting

Signal What it means
401 The credentials are not recognised. Usually the password was copied incompletely, has since been regenerated, or belongs to a different Sendpit environment than the one you are calling. Every failure looks identical by design, so the response never reveals whether the username exists
429 after a run with a bad password Repeated failures are rate limited. A wrong password for a mailbox that exists charges the mailbox and address counters, so a suite left running with a stale password will hit the mailbox counter first. The correct password is still accepted at that point, so fixing the password clears it. Wait for the Retry-After seconds if the address counter is also exhausted
403 plan_required The mailbox's organization is on Free. Upgrade to Basic or higher
403 mailbox_disabled The mailbox or its organization is disabled. Re-enable it in the interface
429 Rate limit reached. Lower your suite's concurrency or move up a plan
204 every time Nothing matched. Check the filters are nested inside filters, and that your app really sent to this mailbox
503 on wait The waiting service is briefly unavailable. Retry, or fall back to polling GET /messages on a short interval
TLS failure on send Certificate verification failed. Look for an intercepting proxy or a stale CA bundle in your image rather than disabling the check
Send rejected at capacity The organization has reached its stored-message limit. Delete messages or upgrade

7. Keeping a Suite Healthy

  • Delete as you go. Stored messages count against your plan's capacity across the whole organization. A suite that never clears up will eventually have its mail refused.
  • One mailbox per environment. Separate mailboxes for local, CI and staging keep one developer's run from matching another's filters.
  • Unique recipients per test. Generating an address per test turns every filter into an exact match and makes parallel runs safe.
  • Send an idempotency key in X-Idempotency-Key on write requests if your suite retries them, so a retry does not duplicate the effect.
  • Treat the credentials as secrets. In CI they belong in your secret store, not in a committed config file. The same pair grants SMTP and API access, so a leak costs you both.

8. For Your AI Coding Assistant

If you use an AI coding assistant, paste the following into your project's CLAUDE.md, AGENTS.md or equivalent so it configures and asserts email correctly without being told each time.

## Email testing

Email is tested against Sendpit, a capture-only SMTP sandbox: mail sent through
it is never delivered to real people. Point the application's mailer at
`smtp.sendpit.com` on port 587 with STARTTLS, using the mailbox's SMTP username
and password, and leave TLS certificate verification enabled. Sendpit routes a
message by the SMTP username that authenticated, not by the recipient address,
so any `To:` address is safe and no recipient logic needs changing for tests.

To assert on a sent email, read it back from `https://sendpit.com/api/v1` using
HTTP Basic with the same SMTP username and password the application sends with,
so reads and writes always address the same mailbox. A test clears the mailbox with
`DELETE /messages`, performs the action that sends mail, then calls
`POST /messages/wait` with its filters nested inside a `filters` object (a flat
payload matches nothing and looks exactly like a timeout), and asserts on the
returned subject, body or links. `wait` returns 200 with matches and 204 when
the window closes empty. API access requires the Basic plan or higher.

Last updated: August 2026

Questions? Contact support@sendpit.com