View as markdown ↓

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

npm install postfleet

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

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:

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.

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
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.

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.