---
title: TypeScript SDK
description: Install the Postfleet TypeScript SDK, send and wait for email from application code, and handle errors and idempotency.
updatedAt: "2026-09-07"
---

# TypeScript SDK

The `postfleet` package is a typed client for the `/api/v1` REST API. Responses come back verbatim — no stripping, no synthetic wrappers. Node 18+ (global `fetch`).

## Install

```bash
npm install postfleet
```

A `pf_` API key from https://postfleet.ai. Override the origin with `baseUrl` for staging or self-hosted.

```ts
import { Postfleet } from 'postfleet';

const pf = new Postfleet({ apiKey: process.env.POSTFLEET_API_KEY! });
```

## Quickstart

Create a mailbox, wait for a reply, then send in-thread:

```ts
const box = await pf.mailboxes.create({ display_name: 'Support agent' });

const incoming = await pf.messages.waitFor({ mailbox_id: box.id });
if (!incoming) throw new Error('timed out waiting for mail');

// Read body_clean: the sanitized text. body_raw is the pre-sanitization original,
// kept for audit — the exact surface the screening pipeline stripped.
console.log(incoming.body_clean);

await pf.messages.send({
  mailbox_id: box.id,
  reply_to_message_id: incoming.id,
  text: 'Got it — working on this now.',
  client_id: 'reply-1',
});
```

## Resources

| Method | Endpoint |
|---|---|
| `pf.mailboxes.create` | `POST /api/v1/mailboxes` |
| `pf.mailboxes.list` | `GET /api/v1/mailboxes` |
| `pf.domains.list` | `GET /api/v1/domains` |
| `pf.domains.create` | `POST /api/v1/domains` |
| `pf.domains.verify` | `POST /api/v1/domains/{id}/verify` |
| `pf.messages.list` | `GET /api/v1/messages?mailbox_id&limit` |
| `pf.messages.get` | `GET /api/v1/messages/{id}` |
| `pf.messages.send` | `POST /api/v1/send` |
| `pf.messages.waitFor` | polls `GET /api/v1/messages` then `GET /api/v1/messages/{id}` |
| `pf.drafts.create` | `POST /api/v1/drafts` |
| `pf.drafts.list` | `GET /api/v1/drafts?mailbox_id` |
| `pf.drafts.get` | `GET /api/v1/drafts/{id}` |
| `pf.drafts.update` | `PATCH /api/v1/drafts/{id}` |
| `pf.drafts.delete` | `DELETE /api/v1/drafts/{id}` |
| `pf.drafts.send` | `POST /api/v1/drafts/{id}/send` |

`messages.send` and `drafts.send` return the sent shape on `200`/`201`, or `{ draft_id, status: "pending_approval" }` on `202` when the mailbox requires a human. That is a success — do not retry. See [Drafts & human-in-the-loop](/docs/drafts).

## `waitFor`

`pf.messages.waitFor({ mailbox_id, from_contains?, subject_contains?, timeout_ms?, poll_interval_ms? })` polls the inbox until a matching **inbound** message arrives whose `created_at` is at or after the call started. Optional `from_contains` / `subject_contains` are case-insensitive substrings. Defaults: timeout 60 000 ms, poll every 2 000 ms. Returns the full message on a hit, or `null` on timeout.

## Errors

Non-2xx responses and network failures throw `PostfleetError`:

- `status` — HTTP status, or `0` when `fetch` itself throws
- `code` — API `code` when present (`key_scope`, `delivery_outcome_unknown`, …)
- `retryable` — API `retryable` when present; `true` on network failure
- `body` — parsed JSON, or `{}` if the body was not JSON

```ts
import { PostfleetError } from 'postfleet';

try {
  await pf.messages.send({ mailbox_id, to, subject, text, client_id });
} catch (err) {
  if (err instanceof PostfleetError && err.retryable) {
    // retry with the SAME client_id
  }
  throw err;
}
```

## Idempotency

`client_id` stays optional and the SDK never mints one. Generate a `client_id` once per logical send and reuse it on retry; never mint a new one per attempt. A fresh key per attempt is exactly the double-send the key prevents. See [Sending & idempotency](/docs/sending).

## Not in v0.1

No retries with backoff, and no Python SDK. Both are later, separate decisions.

The full request and response shapes live in the [API reference](/docs/api).
