TypeScript SDK
npm install @tropmail/sdkZero 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" });Search
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.
| Class | HTTP |
|---|---|
ValidationError | 400, or local key check |
AuthenticationError | 401 |
TierError | 403 |
NotFoundError | 404 |
RateLimitError | 429 |
MarkdownTimeoutError | 504 |
ServerError | 5xx |
ConnectionError | transport 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
| File | What it shows |
|---|---|
quickstart.ts | mailbox summary, listing, reading one message |
triage.ts | auto-paging, bulk actions, AbortSignal |
worker.ts | fetch handler entry |