12 · MailMarco API
SDK & CLI
A thin typed wrapper over the same HTTP routes. Everything it does, you can do with curl — it just saves you the header plumbing.
Install
npm install @mailmarco/sdk-nodeThe package is ESM-only and targets Node 18 or newer, because it uses the global fetch. There are no runtime dependencies.
Creating a client
import { MailMarco } from "@mailmarco/sdk-node";
const mm = new MailMarco({
apiKey: process.env.MAILMARCO_API_KEY!,
baseUrl: "https://api.mailmarco.com",
});Every call sends authorization: Bearer <apiKey> and content-type: application/json. The SDK covers the API-key surface only — domain management through the SDK will fail with a 401, because those routes want a session token.
Emails
// Send. The second argument sets the Idempotency-Key header.
const sent = await mm.emails.send(
{
from: "[email protected]",
to: ["[email protected]"],
subject: "Your receipt",
html: "<p>Thanks for your order.</p>",
text: "Thanks for your order.",
tags: { kind: "receipt" },
},
{ idempotencyKey: "order-2291-receipt" },
);
console.log(sent.id, sent.status); // "b1d2…", "queued"
// Retrieve, including the event timeline.
const message = await mm.emails.get(sent.id);
// Cancel a scheduled message.
await mm.emails.cancel(sent.id);Domains
const domains = await mm.domains.list();
const created = await mm.domains.create("mail.example.com");
const records = await mm.domains.dnsRecords(created.id);
await mm.domains.verify(created.id);These call the same routes as the Domains page, which are session-authenticated — so they only work if you construct the client with a session access token in apiKey. That works because the SDK just puts the value in the bearer header.
Suppressions
await mm.suppressions.list();
await mm.suppressions.add("[email protected]"); // scope defaults to "tenant"
await mm.suppressions.add("example.net", "domain"); // block a whole domain
await mm.suppressions.remove(suppressionId);Error handling
Any non-2xx throws a MailMarcoError carrying the HTTP status and the API's error field, so you can branch on the same codes documented in Errors & limits.
import { MailMarco, MailMarcoError } from "@mailmarco/sdk-node";
try {
await mm.emails.send(input, { idempotencyKey: key });
} catch (err) {
if (!(err instanceof MailMarcoError)) throw err;
switch (err.status) {
case 402:
// err.code === "plan_limit_exceeded"
await alertBilling(err.message);
break;
case 403:
// err.code === "org_suspended"
await pauseOutbox();
break;
case 429:
// err.code === "warmup_cap_reached" — retry tomorrow, not in a few seconds
await deferUntilTomorrow();
break;
case 400:
// validation or a domain rule — never retry unchanged
logPermanentFailure(err.message);
break;
default:
throw err;
}
}| Property | Value |
|---|---|
status | The HTTP status code. |
code | The response's error field, or "error" when absent. |
message | The response's message, falling back to the code and then to HTTP <status>. |
OpenAPI spec
A machine-readable OpenAPI 3.1.0 description of the API is served from this site:
curl https://docs.mailmarco.com/openapi.json -o mailmarco.openapi.json
# generate a client, or import it into an HTTP tool
npx @openapitools/openapi-generator-cli generate \
-i mailmarco.openapi.json -g typescript-fetch -o ./generatedThe document is authored against the real controllers and schemas — including the success statuses that surprise people, such as 201 on /cancel and /verify. It declares two bearer security schemes, apiKey and session, so each operation states which credential it wants.
| Covered | Not yet covered |
|---|---|
| emails, domains, templates, suppressions, bounces, complaints, webhooks, api-keys | marketing, inbound, reputation, billing, admin |
The uncovered areas are fully documented in prose on this site — see Marketing and Inbound — they are simply not in the spec document yet.