Home
Official SDKs

TypeScript SDK

Zero-dependency @tropmail/sdk for Node, Bun, Deno, and the browser.
npm install @tropmail/sdk

Zero runtime dependencies. Built on fetch, so it runs on Node 18+, Bun, Deno, and other fetch runtimes, including the browser.

On Node the key is read from TROPMAIL_API_KEY when you omit apiKey.

Source: github.com/tropmail/tropmail-sdk

Quick start

import { TropMail } from "@tropmail/sdk";

const client = new TropMail({ apiKey: process.env.TROPMAIL_API_KEY! });

const { mailboxes } = await client.mailboxes.list();
const mailbox = mailboxes[0]!;
const mailboxId = mailbox.id;
console.log(`${mailbox.email}: ${mailbox.opened_count} opened`);

for await (const email of client.emails.iterate({ mailboxId, status: "Open" })) {
  console.log(email.timestamp, email.from.address, email.subject);
}

Fetch runtimes

No node: imports. The same client works anywhere fetch exists:

export default {
  async fetch(_request: Request, env: { TROPMAIL_API_KEY: string }): Promise<Response> {
    const client = new TropMail({ apiKey: env.TROPMAIL_API_KEY });
    const { mailboxes } = await client.mailboxes.list();
    const page = await client.emails.list({ mailboxId: mailboxes[0].id, limit: 25 });
    return Response.json(page);
  },
};

Reading an email

get() returns the HTML view by default. Pass the email object you already have rather than a bare id and the SDK forwards its timestamp for a faster lookup.

const page = await client.emails.list({ mailboxId, limit: 10 });
const email = page.emails[0]!;

const detail = await client.emails.get(mailboxId, email, { view: "text" });
console.log(detail.content);

for (const attachment of detail.attachments) {
  console.log(attachment.filename, attachment.size, attachment.scan_status);
}

Markdown

The markdown view is generated on demand. getMarkdown() retries 504 for you:

const detail = await client.emails.getMarkdown(mailboxId, email);

Actions

await client.emails.favorite(mailboxId, email);
await client.emails.close(mailboxId, email);
await client.emails.block(mailboxId, email);
await client.emails.clearAction(mailboxId, email);

Those are shorthands for update():

await client.emails.update(mailboxId, email, { emailState: "Open", actionStatus: "Favorite" });
const page = await client.emails.search({ mailboxId, query: "invoice", limit: 25 });

for await (const email of client.emails.searchIterate("invoice", { mailboxId })) {
  console.log(email.subject);
}

Search responses report total: 0. Iterators page until they see a short page.

Attachments

const info = await client.attachments.get(mailboxId, attachmentId);

await client.attachments.scan(mailboxId, attachmentId);
let meta = await client.attachments.get(mailboxId, attachmentId);
while (meta.scan_status === "Processing") {
  await new Promise((r) => setTimeout(r, 2000));
  meta = await client.attachments.get(mailboxId, attachmentId);
}
console.log(meta.scan_status, meta.scan_result);

const bytes = await client.attachments.downloadBytes(mailboxId, attachmentId);

const response = await client.attachments.download(mailboxId, attachmentId);
await response.body?.pipeTo(destination);

download() returns a fetch Response, not a URL. fetchContent() is the same call. A file whose size is 0 is a real empty attachment; the download is 200 with an empty body.

Downloads use your API key on the same host as the rest of the API.

Errors

import { NotFoundError, RateLimitError, TropMailError } from "@tropmail/sdk";

try {
  await client.emails.get(mailboxId, "does-not-exist");
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(error.status, error.requestId);
  } else if (error instanceof RateLimitError) {
    console.log("retry after", error.retryAfter);
  } else if (error instanceof TropMailError) {
    console.log("request failed:", error.message);
  }
}

Every error carries status, message, and requestId.

A missing or malformed key fails locally as ValidationError before any HTTP call. HTTP 400 from the API is the same class.

ClassHTTP
ValidationError400, or local key check
AuthenticationError401
TierError403
NotFoundError404
RateLimitError429
MarkdownTimeoutError504
ServerError5xx
ConnectionErrortransport failure

Rate limits

The client reads X-RateLimit-* headers and stays within your account limit.

console.log(client.rateLimit);

Pass throttle: false if you manage concurrency yourself.

Cancellation and timeouts

Every method takes signal and timeout:

const controller = new AbortController();
setTimeout(() => controller.abort(), 1_000);

await client.emails.list({ mailboxId, signal: controller.signal });
await client.emails.list({ mailboxId, timeout: 5_000 });

Configuration

new TropMail({
  apiKey: "…",                                  // else TROPMAIL_API_KEY
  baseUrl: "https://api.tropmail.com/api/v1",
  timeout: 120_000,
  maxRetries: 3,
  throttle: true,
  fetch: customFetch,
});

Retries with backoff on 429, 502, 503, 504, and transport errors. Reads retry. Mutations do not.

Examples

FileWhat it shows
quickstart.tsmailbox summary, listing, reading one message
triage.tsauto-paging, bulk actions, AbortSignal
worker.tsfetch handler entry