TypeScript SDK for the Mailneo public API (v2).
- Zero runtime dependencies — built on
fetch. Node 18+ and modern browsers. - Types generated from the OpenAPI spec (
openapi/openapi.json), so the compiler knows exactly what every endpoint accepts and returns. - ESM and CommonJS builds.
npm install @mailneo/sdkimport { MailneoClient } from "@mailneo/sdk";
const client = new MailneoClient({ apiKey: process.env.MAILNEO_API_KEY! });
// or, with an OAuth 2.1 access token:
// const client = new MailneoClient({ accessToken });
// Who am I?
const me = await client.me();
console.log(me.data.scopes);
// Automatic cursor pagination — pages are fetched lazily as you iterate.
for await (const subscriber of client.subscribers.list({ status: "ENABLED" })) {
console.log(subscriber.email);
}
// Create, with an Idempotency-Key so the write is safely retryable.
const created = await client.subscribers.create(
{ email: "ada@example.com", first_name: "Ada" },
{ idempotencyKey: "create-ada-2026-08-04" }
);One namespace per API tag: subscribers, lists, segments, contacts,
campaigns, newsletters, templates, suppressions, analytics,
accounts, webhooks, system (plus client.me() / client.usage()
shortcuts). The namespaces are generated from the OpenAPI spec by
npm run generate; only the thin core (auth, retries, pagination, errors) is
hand-written.
Methods return the API's envelope as-is: { data, meta }. meta.request_id
is what Mailneo support asks for; on a dry_run call, data is null and
meta.impact describes what would have happened.
List methods return a lazy CursorPage:
const page = client.subscribers.list({ limit: 100 });
for await (const item of page) { /* items across every page */ }
for await (const envelope of page.pages()) { /* raw pages */ }
await page.firstPage(); // exactly one request
await page.toArray(500); // bounded drainFor incremental sync, read meta.synced_through from the final page
(has_more: false) and pass it back as updated_since on the next run. The
server deliberately omits it mid-walk; the pages() iterator is how you reach
it. Expect to occasionally re-see a row — upsert by id.
- Retried: HTTP 429, 500, 502, 503, 504, and connection failures.
- A request is only retried when repeating it is safe: idempotent methods
(GET/PUT/DELETE), or a write carrying an
Idempotency-Key. APOST/PATCHwithout a key is never retried — pass{ idempotencyKey }to opt in. Retry-Afteris respected exactly (seconds or HTTP-date). If it exceedsmaxRetryAfterMs(default 60 s) the SDK throws instead of stalling.- Otherwise: full-jitter exponential backoff (
retryBaseMs500 ms, doubling, capped atretryCapMs30 s). DefaultmaxRetriesis 2.
Non-2xx responses throw:
MailneoApiError— the API's error envelope, withstatus,type,code(stable, branch on it),message,param,docUrl,requestId,retryAfterMs.MailneoHttpError— a non-2xx whose body was not the envelope (proxy pages, HTML). No fabricatedcode.MailneoConnectionError—fetchitself failed.
new MailneoClient({
apiKey, // or accessToken — one is required
baseUrl, // default https://api.mailneo.co/api/v2
fetch, // injectable (tests, polyfills)
maxRetries, // default 2
retryBaseMs, // default 500
retryCapMs, // default 30_000
maxRetryAfterMs, // default 60_000
defaultHeaders, // merged under auth headers
});Per-write options: { idempotencyKey, dryRun, signal, headers }. Operations
the spec marks x-mailneo-idempotency: required (e.g. contacts.bulkUpsert,
webhooks.replayDelivery) require idempotencyKey at the type level.
npm ci
npm run generate # regen src/generated/* from openapi/openapi.json
npm run typecheck # includes examples/usage.ts, the compile-time smoke test
npm test # vitest, mocked fetch, no network
npm run build # tsup: ESM + CJS + d.tsopenapi/openapi.json is a vendored snapshot of the spec the Mailneo API
publishes, and src/generated/ is committed so the package builds standalone;
refresh the snapshot and regenerate whenever the API spec changes.