Migrate from Mailsac

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

Why switch?

ConcernMailsacOneShotMail
ModelCatch-all domains with persistent inboxes.One-shot addresses with auto-deletion.
IsolationShared domains risk inbox collision.Cryptographically random, guaranteed unique.
CleanupManual deletion or expiry rules.Automatic TTL + bulk delete by label.
SendingLimited outbound support.Full one-shot sending with attachments.
SDK qualityNode.js SDK, REST API.SDKs for Python, Go, Ruby, JS, Java.
wait_for_email()Websocket/webhook available.Built into every SDK with exponential backoff.
AI agent supportNone.MCP server for Claude, Cursor, Windsurf.
PricingFree tier + paid tiers.Free tier + $10/$79 paid plans.

Feature mapping

Mailsac featureOneShotMail equivalent
Catch-all domain inboxoneshot.create() — dedicated, one-shot address
Custom domainin.oneshotemail.com / out.oneshotemail.com (custom domains in future)
GET /addresses/{email}/messagesoneshot.get_email(address_id)
GET /addresses/{email}/messages/{id}oneshot.get_email(address_id)
Websocket message streamoneshot.wait_for_email(address_id)
Webhook forwardingComing soon. Use wait_for_email() for now.
Message listoneshot.list(label="...")
Delete messageoneshot.delete(address_id)
Reserve addressoneshot.create(ttl_seconds=...)
Outbound SMTPoneshot.send(to=..., subject=..., ...)

Code comparison

Before: Mailsac (JavaScript)

const Mailsac = require("@mailsac/api");

const mailsac = new Mailsac({ headers: { "Mailsac-Key": process.env.MAILSAC_KEY } });

async function testSignup() {
  const inbox = `test-${Date.now()}@my-domain.mailsac.com`;

  // Trigger signup
  await app.signup({ email: inbox });

  // Poll for messages
  let messages;
  for (let i = 0; i < 30; i++) {
    const { data } = await mailsac.messages.listMessages(inbox);
    messages = data;
    if (messages.length > 0) break;
    await new Promise((r) => setTimeout(r, 2000));
  }

  if (!messages?.length) throw new Error("No email received");

  // Fetch the message
  const { data: msg } = await mailsac.messages.getFullMessage(inbox, messages[0]._id);
  assert(msg.subject.includes("Verify"));

  // Cleanup
  await mailsac.messages.deleteAllMessages(inbox);
}

After: OneShotMail (JavaScript)

import { OneShotClient } from "oneshot-mail";

const oneshot = new OneShotClient(process.env.ONESHOT_API_KEY);

async function testSignup() {
  const addr = await oneshot.create({ ttl: 300, label: "test-signup" });

  await app.signup({ email: addr.address });

  const email = await oneshot.waitForEmail(addr.id, { timeout: 30000 });
  assert(email.subject.includes("Verify"));
  // No manual cleanup needed -- TTL handles it
}

Before: Mailsac (Python via REST)

import requests
import time

def test_signup():
    inbox = f"test-{int(time.time())}@my-domain.mailsac.com"
    headers = {"Mailsac-Key": os.environ["MAILSAC_KEY"]}

    app.signup(email=inbox)

    for _ in range(30):
        resp = requests.get(
            f"https://mailsac.com/api/addresses/{inbox}/messages",
            headers=headers,
        )
        messages = resp.json()
        if messages:
            msg_id = messages[0]["_id"]
            msg_resp = requests.get(
                f"https://mailsac.com/api/addresses/{inbox}/messages/{msg_id}",
                headers=headers,
            )
            msg = msg_resp.json()
            assert "Verify" in msg["subject"]
            # Cleanup
            requests.delete(
                f"https://mailsac.com/api/addresses/{inbox}/messages",
                headers=headers,
            )
            return
        time.sleep(2)
    raise TimeoutError("No email received")

After: OneShotMail (Python)

import oneshot

def test_signup():
    addr = oneshot.create(ttl_seconds=300, label="test-signup")
    app.signup(email=addr.address)
    email = oneshot.wait_for_email(addr.id, timeout=30)
    assert "Verify" in email.subject

Migration steps

1. Install the SDK

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

2. Get an API key

Register at app.oneshotemail.com.

3. Replace inbox creation

  • Mailsac: Generate an email at your Mailsac domain, optionally reserve it.
  • OneShotMail: Call create() to get a unique address. No domain setup required.

4. Replace message polling

  • Mailsac: List messages endpoint + polling loop (or websockets for real-time).
  • OneShotMail: wait_for_email() handles everything.

5. Replace message fetching

  • Mailsac: Two-step: list messages, then fetch by message ID.
  • OneShotMail: One-step: get_email(address_id) returns the full email.

6. Replace cleanup

  • Mailsac: DELETE /addresses/{email}/messages to clean up.
  • OneShotMail: Automatic via TTL. Or delete(address_id) / delete_by_label(label) for immediate cleanup.

7. Update CI secrets

Replace MAILSAC_KEY with ONESHOT_API_KEY in your CI/CD configuration.

Gotchas

  1. One email per address. Mailsac inboxes can receive multiple messages. OneShotMail addresses accept exactly one. Create a new address for each expected email.

  2. No custom domains (yet). Mailsac lets you use custom catch-all domains. OneShotMail currently uses in.oneshotemail.com for receiving and out.oneshotemail.com for sending. Custom domain support is on the roadmap.

  3. No websocket streaming. Mailsac offers websocket-based real-time notifications. OneShotMail uses polling with exponential backoff via wait_for_email(). For most test suites, this is functionally equivalent and simpler.

  4. Different JSON structure. Mailsac uses _id, subject, from, etc. OneShotMail uses id, subject, from_address (Python SDK) / from (API), text_body, html_body. Update your assertions accordingly.

  5. Address format. Mailsac addresses are name@domain.mailsac.com. OneShotMail receive addresses are random_id@in.oneshotemail.com and send addresses are random_id@out.oneshotemail.com. The local part is the address ID used for all API operations.