Product

TypeScript and Python SDKs are here

TypeScript and Python SDKs are here

Testing email flows just got a lot simpler. We're releasing official SDKs for TypeScript and Python — designed to replace the boilerplate of polling loops, manual fetch calls, and untyped JSON parsing.

#The problem with raw API calls

Every team using Plop for E2E tests ends up writing the same helper:

// Every project has a version of this
async function waitForEmail(to, timeout = 10000) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    const res = await fetch(`https://api.plop.email/v1/messages/latest?to=${to}`, {
      headers: { Authorization: `Bearer ${process.env.PLOP_API_KEY}` },
    });
    if (res.ok) return (await res.json()).data;
    await new Promise(r => setTimeout(r, 1000));
  }
  throw new Error("Timeout");
}

It works, but it's repetitive, untyped, and easy to get wrong (error handling, timeout edge cases, race conditions with since timestamps).

#One line instead

#TypeScript

npm install @plop-email/sdk
import { Plop } from "@plop-email/sdk";

const plop = new Plop(); // reads PLOP_API_KEY from env

const email = await plop.messages.waitFor(
  { mailbox: "qa", tag: "verification" },
  { timeout: 30_000 },
);

const otp = email.textContent?.match(/\d{6}/)?.[0];

That's it. waitFor polls the API, handles 404s, respects the timeout, and returns a fully typed MessageDetail object. If nothing arrives, it throws a PlopError.

#Python

pip install plop-sdk
from plop_sdk import Plop

plop = Plop()  # reads PLOP_API_KEY from env

email = plop.messages.wait_for(
    mailbox="qa",
    tag="verification",
    timeout=30,
)

import re
otp = re.search(r"\d{6}", email.text_content).group()

Sync and async clients included. AsyncPlop works with pytest-asyncio and async with context managers.

#What's included

Both SDKs cover the full Plop API:

FeatureTypeScriptPython
List mailboxesplop.mailboxes.list()plop.mailboxes.list()
List/filter messagesplop.messages.list({...})plop.messages.list(...)
Get message by IDplop.messages.get(id)plop.messages.get(id)
Get latest matchplop.messages.latest({...})plop.messages.latest(...)
Poll for emailplop.messages.waitFor(...)plop.messages.wait_for(...)
Verify webhooksplop.webhooks.verify(...)plop.webhooks.verify(...)
Create/update/delete mailboxesplop.mailboxes.create(...)plop.mailboxes.create(...)
Delete messagesplop.messages.delete(id)plop.messages.delete(id)
Stream messages (SSE)plop.messages.stream(...)plop.messages.stream(...)
Manage webhooksplop.webhooks.create(...)plop.webhooks.create(...)
List webhook deliveriesplop.webhooks.deliveries(id)plop.webhooks.deliveries(id)
Rotate API keyplop.apiKeys.rotate()plop.api_keys.rotate()
Cursor paginationafter_id + has_moreafter_id + has_more

#Design decisions

Zero runtime dependencies (TypeScript) — uses native fetch and crypto. No axios, no node-fetch. Works everywhere Node 18+ runs.

httpx + Pydantic (Python) — sync and async HTTP with typed Pydantic models. Every response field has a proper Python type.

{data, error} pattern (TypeScript) — inspired by Resend. Methods never throw; you destructure the result. Exception: waitFor throws on timeout since it's typically used in test contexts where throwing is natural.

Typed exceptions (Python) — PlopAuthError, PlopForbiddenError, PlopNotFoundError, PlopTimeoutError. Catch exactly what you need.

#Get started

# TypeScript
npm install @plop-email/sdk

# Python
pip install plop-sdk

Set your PLOP_API_KEY environment variable and you're ready. Both SDKs auto-detect the key from the environment.

Alex Vakhitov
Alex VakhitovFounder & CEO, Plop