Migrate from Mailinator

Side-by-side comparison and step-by-step migration guide from Mailinator to OneShotMail.

Why switch?

ConcernMailinatorOneShotMail
Public inboxesFree tier uses shared, public inboxes.Every address is private and cryptographically random.
Pricing$79-$495+/month.Free tier + $10-$79/month paid plans.
Test isolationShared inboxes. Risk of cross-contamination.One-shot guarantee: each address gets one email.
SendingNo outbound send API.Built-in one-shot sending.
SDK qualityREST API, no official SDKs.Official SDKs for Python, Go, Ruby, JS, Java.
wait_for_email()Not available. Roll your own polling.Built into every SDK with exponential backoff.
CleanupManual. Inboxes persist.Automatic via TTL + bulk delete by label.
AI agent supportNone.MCP server for Claude, Cursor, Windsurf.

Feature mapping

Mailinator featureOneShotMail equivalent
Public inboxoneshot.create() — private, isolated address
Private inbox (paid)oneshot.create() — all addresses are private
Inbox fetch via APIoneshot.get_email(address_id)
Team domainsLabels for grouping. Custom domains in future.
Message listoneshot.list(label="...")
Delete messageoneshot.delete(address_id)
Rules engineNot needed — one-shot guarantee handles routing.
Webhooks (paid)Coming soon. Use wait_for_email() for now.
SMS testingNot supported (different product focus).

Code comparison

Before: Mailinator (Python, manual HTTP)

import requests
import time

MAILINATOR_API_KEY = "your-mailinator-key"
DOMAIN = "your-domain.testinator.com"

def test_signup():
    # Create a random inbox name (no real isolation)
    inbox = f"test-{int(time.time())}"
    email = f"{inbox}@{DOMAIN}"

    # Trigger signup
    app.signup(email=email)

    # Poll for email (no SDK, manual implementation)
    for _ in range(30):
        resp = requests.get(
            f"https://mailinator.com/api/v2/domains/{DOMAIN}/inboxes/{inbox}",
            headers={"Authorization": MAILINATOR_API_KEY},
        )
        messages = resp.json().get("msgs", [])
        if messages:
            # Fetch the first message
            msg_id = messages[0]["id"]
            msg_resp = requests.get(
                f"https://mailinator.com/api/v2/domains/{DOMAIN}/inboxes/{inbox}/messages/{msg_id}",
                headers={"Authorization": MAILINATOR_API_KEY},
            )
            msg = msg_resp.json()
            assert "Verify" in msg["subject"]
            return
        time.sleep(2)

    raise TimeoutError("No email received")

After: OneShotMail (Python)

import oneshot

def test_signup():
    # Create an isolated, private address
    addr = oneshot.create(ttl_seconds=300, label="test-signup")

    # Trigger signup
    app.signup(email=addr.address)

    # Wait for email (built-in polling with backoff)
    email = oneshot.wait_for_email(addr.id, timeout=30)
    assert "Verify" in email.subject

Lines of code: 25 —> 7. Dependencies: requests + manual polling —> oneshot-mail with built-in everything.

Before: Mailinator (Ruby, manual HTTP)

require "faraday"
require "json"

def test_signup
  inbox = "test-#{Time.now.to_i}"
  email = "#{inbox}@your-domain.testinator.com"

  MyApp.signup(email: email)

  # Poll for email
  30.times do
    resp = Faraday.get(
      "https://mailinator.com/api/v2/domains/your-domain.testinator.com/inboxes/#{inbox}",
      nil,
      { "Authorization" => ENV["MAILINATOR_API_KEY"] }
    )
    messages = JSON.parse(resp.body)["msgs"]
    if messages&.any?
      msg_id = messages.first["id"]
      # ... fetch full message ...
      return
    end
    sleep 2
  end
  raise "Timeout"
end

After: OneShotMail (Ruby)

require "oneshot"

def test_signup
  client = OneShot::Client.new
  addr = client.create(ttl: 300, label: "test-signup")

  MyApp.signup(email: addr["address"])

  email = client.wait_for_email(addr["id"], timeout: 30)
  expect(email["subject"]).to include("Verify")
end

Migration steps

1. Install the SDK

pip install oneshot-mail   # Python
# or
gem install oneshot-mail   # Ruby
# or
npm install oneshot-mail   # JavaScript
# or
go get github.com/oneshotmail/oneshot-go  # Go

2. Get an API key

Sign up at app.oneshotemail.com. Free tier for evaluation; Solo plan ($10/month) for production test suites.

3. Replace inbox creation

  • Mailinator: Generate a random inbox name string.
  • OneShotMail: Call create() which returns a guaranteed-unique, cryptographically random address.

4. Replace polling logic

  • Mailinator: Custom polling loop with time.sleep().
  • OneShotMail: wait_for_email() handles polling, backoff, timeout, and error handling.

5. Replace email fetching

  • Mailinator: Separate calls to list messages, then fetch individual message by ID.
  • OneShotMail: get_email() returns the full parsed email in one call.

6. Add labels for cleanup

Tag your addresses with the CI run ID:

addr = oneshot.create(label=f"ci-{os.environ['BUILD_ID']}")

7. Add cleanup

In your test teardown:

oneshot.delete_by_label(f"ci-{os.environ['BUILD_ID']}")

8. Update CI secrets

Replace MAILINATOR_API_KEY with ONESHOT_API_KEY in your CI/CD secrets.

Gotchas

  1. One email per address. Mailinator inboxes can hold multiple messages. OneShotMail addresses accept exactly one email. If your test triggers multiple emails to the same address, create multiple addresses.

  2. No persistent inboxes. Mailinator inboxes persist (on paid plans). OneShotMail addresses auto-delete. If you need to reference old emails, save the content before the TTL expires.

  3. TTL is mandatory. Every OneShotMail address has a TTL. Mailinator inboxes persist until manually deleted. Set your TTL with enough margin for your test to complete.

  4. Different email format. Mailinator returns email in its own JSON format. OneShotMail returns a different JSON structure. Update your assertions to use the OneShotMail field names (text_body, html_body, from, etc.).

  5. No shared/public inboxes. If you were using Mailinator’s public inboxes (free tier), those are intentionally not replicated. Every OneShotMail address is private.