Skip to content

Developers

TMailr API

A small HTTP API for disposable inboxes and developer sandboxes. Create an address, let your app send to it, and read what arrived with the one-time code already pulled out. JSON in, JSON out, over https://api.tmailr.com.

Overview

Everything is JSON over HTTPS. There are two ways to hold an address: a token, handed back when you create something and passed as ?token= or a Bearer header, and an API key, which owns many sandboxes at once and is what CI uses. The temporary inbox needs no account and no key; a key is what makes a run repeatable.

  • Disposable inbox — one throwaway address that expires within the hour.
  • Sandbox — a catch-all that lasts a year; every +suffix lands in the same place, so one sandbox covers a whole test suite.
  • Extracted code — every message carries otp, the verification code or sign-in link if there is one, so you never parse a body.

Authentication

Create a key once and keep the secret in your CI secrets. It is shown a single time; we store a hash and the first few characters, enough to recognise it in a list and not enough to use.

POST/v1/keyscreate a key (no auth; per-IP limited)
request
POST /v1/keys
{ "name": "ci" }

-> 201
{ "key": { "id": "...", "name": "ci", "prefix": "tk_ab12" },
  "secret": "tk_ab12..." }   // shown once

Send the secret on every keyed call:

Authorization: Bearer tk_ab12...
GET/v1/keys/currentthe key's own details
DELETE/v1/keys/currentrevoke it

You can also see, name and revoke keys in your account.

Quick start

The whole flow: make a sandbox, let the app under test send to any +suffixon it, then wait for what arrived. No key needed to try by hand; a key makes it repeatable.

# 1) an API key, kept in CI secrets. The secret is shown once.
curl -sX POST https://api.tmailr.com/v1/keys \
  -H 'content-type: application/json' -d '{"name":"ci"}'
# -> { "key": { "prefix": "tk_..." }, "secret": "tk_..." }

# 2) a sandbox to capture this run's mail
curl -sX POST https://api.tmailr.com/v1/sandboxes \
  -H 'authorization: Bearer tk_...' \
  -H 'content-type: application/json' -d '{"name":"checkout"}'
# -> { "sandbox": { "id": "SID", "address": "checkout@etymail.com" },
#      "catchAll": "checkout+anything@etymail.com" }

# 3) after your app sends mail, read what arrived
curl -s https://api.tmailr.com/v1/resources/SID/messages -H 'authorization: Bearer tk_...'
# -> { "messages": [ { "subject": "Verify", "otp": "123456" } ] }

Disposable inboxes

A throwaway address with no account. The response carries a token; keep it and use it to read, extend or burn the inbox.

GET/v1/inbox-domainswhich domains you can ask for
POST/v1/inboxescreate one (name and domain optional)
request
POST /v1/inboxes
{ "name": "invoices", "domain": "kahmail.com" }   // both optional

-> 201
{ "inbox": { "id": "...", "address": "invoices@kahmail.com",
             "kind": "guest", "expiresAt": "2026-01-01T12:00:00Z" },
  "token": "..." }
GET/v1/inboxes/current/messages?token=…list, newest first
POST/v1/inboxes/current/extend?token=…add another hour, up to 24
DELETE/v1/inboxes/current?token=…burn it now

Live updates: GET /v1/inboxes/current/events?token=… is a Server-Sent Events stream that pushes each message as it lands.

Developer sandboxes

A catch-all built for a test suite: longer-lived than a throwaway inbox, and every address on it works without creating anything first. Owned by an API key so a suite that makes one per run does not have to keep a token per run.

POST/v1/sandboxesBearer key; name optional
request
POST /v1/sandboxes            Authorization: Bearer tk_...
{ "name": "checkout" }

-> 201
{ "sandbox": { "id": "SID", "address": "checkout@etymail.com" },
  "token": "...",
  "catchAll": "checkout+anything@etymail.com" }
GET/v1/keys/current/resourcesevery sandbox this key owns
GET/v1/resources/:id/messagesBearer key; newest 100

Paging: when there are more, the response carries nextBefore; pass it back as ?before= and loop until it is null. Ask for a different size with ?limit=, up to 200.

Reading messages

Each message in a listing looks like this:

message
{
  "id": "...",
  "fromAddr": "no-reply@shop.com",
  "fromName": "Shop",
  "subject": "Verify your email",
  "receivedAt": "2026-01-01T12:00:00.123Z",
  "otp": "123456",          // the extracted code, or null
  "otpKind": "code",        // "code" | "link" | null
  "hasAttachments": false,
  "sizeBytes": 4821
}
GET/v1/messages/:id?token=…one message, with SPF/DKIM/DMARC results, text body, links and attachment list
GET/v1/messages/:id/html?token=…sanitized HTML; images blocked unless ?images=1
GET/v1/messages/:id/raw?token=…the original .eml
GET/v1/messages/:id/attachments/:idx?token=…one attachment, served download-only

Foreign HTML is sanitized before you ever see it and served with a strict Content-Security-Policy; attachments are always Content-Disposition: attachment and dangerous file types are refused outright.

Webhooks

Rather than poll, point a webhook at your own URL and we POST each message as it lands, with the parsed body and any one-time code already extracted.

POST/v1/inboxes/current/webhook{ url }; returns a signing secret once
each delivery carries
X-TMailr-Signature: t=<unix>,v1=<hex>

hex = HMAC-SHA256(secret, "<t>.<raw body>")

Your endpoint is a public URL, so verify every delivery against the raw body before any JSON parsing, and reject a timestamp older than a few minutes. Both SDKs do this for you, with a five-minute window and a constant-time comparison:

import { TMailr } from '@tmailr/sdk';

// rawBody is the exact bytes received, before any JSON parsing
if (!TMailr.verifyWebhook(secret, req.headers['x-tmailr-signature'], rawBody)) {
  return res.status(400).end();
}
const message = JSON.parse(rawBody); // now safe to use

An endpoint that keeps failing is switched off rather than retried forever; set it again to turn it back on, which is also how you rotate a leaked secret.

SDKs

Two thin, dependency-free clients over this API, so a test can send mail and read what arrived without reinventing the polling loop. Publishing to npm and PyPI is in progress; the intended install:

npm i @tmailr/sdk       # TypeScript / Node  (publishing soon)
pip install tmailr      # Python              (publishing soon)

The core is createSandbox, waitForMessageand verifyWebhook (and their snake_casePython twins). See the Quick start above for the full loop.

Free tools API

The checks behind the free tools, callable directly. Deterministic DNS and parse observations, cached five minutes, metered per IP. No key needed.

GET/v1/tools/domain-check?domain=example.comMX, SPF with the ten-lookup count, DMARC policy, MTA-STS, TLS-RPT
GET/v1/tools/check-address?email=user@gmial.comdisposable domains, role mailboxes and typos
POST/v1/tools/analyze-headers{ headers } — delivery path, per-hop delays and auth results

Each returns the underlying records and observations as JSON, the same data the tool pages render in plain English. See tmailr.com/tools for the interactive versions.

Rate limits & errors

Creation and abuse-prone endpoints are limited per IP; a message that arrives when the API is disrupted is refused at the SMTP stage with a temporary error, so senders retry and nothing is silently lost. Standard HTTP status codes throughout:

200 / 201   ok
400         malformed request
403         refused: a rate or abuse limit, or an address you may not use
404         no such resource — also what a bad key or token gets
409         that name is already taken
429         too many free-tool checks; wait and retry

A refusal never reveals whether something exists: an address that is not yours and one that never existed both answer 404. Separately, mail sent to an inbox that is already full gets a temporary452 at the SMTP stage, so the sender retries rather than losing it.

Questions or something missing? The developer sandbox page has a live create button, and the tools live at tmailr.com/tools.