Skip to content

API reference

The Mailfully v1 API.

A REST API over HTTPS with JSON request and response bodies. Every endpoint below is also a one-line call in the official @mailfully/node SDK, which returns a typed { data, error } tuple and never throws.

Endpoints

Base URL https://api.mailfully.com. Construct the client with new Mailfully({ apiKey }); each resource hangs off the instance (mailfully.emails, .domains, and so on).

Emails

Send transactional email (single and batch), read the message log, and manage scheduled sends.

MethodPathSDK callSummary
POST/v1/emailsmailfully.emails.send(input, opts?)Send one email (202 → { id }).
POST/v1/emails/batchmailfully.emails.batch(inputs, opts?)Send up to 100 emails in one request (202 → { data: [{ id }] }).
GET/v1/emails/{id}mailfully.emails.get(id)Fetch one email's detail.
GET/v1/emailsmailfully.emails.list(query?)The keyset-paginated message log with filters.
POST/v1/emails/{id}/cancelmailfully.emails.cancel(id)Cancel a not-yet-sent email.
PATCH/v1/emails/{id}mailfully.emails.reschedule(id, input)Move a future-dated send to a new time.
GET/v1/emails/{id}/eventsmailfully.emails.events(id)The per-message event timeline.

Domains

Add and verify custom sending domains and toggle open/click tracking.

MethodPathSDK callSummary
POST/v1/domainsmailfully.domains.create(input)Add a sending domain (201 → domain + DNS records).
GET/v1/domainsmailfully.domains.list()List this org's domains.
GET/v1/domains/{id}mailfully.domains.get(id)Domain detail with its DNS records.
POST/v1/domains/{id}/verifymailfully.domains.verify(id)Poll SES and persist the new verification status.
POST/v1/domains/{id}/trackingmailfully.domains.tracking(id, input)Toggle open/click tracking.

API keys

Mint, list, revoke, and rotate API keys. The plaintext key is returned exactly once.

MethodPathSDK callSummary
GET/v1/api-keysmailfully.apiKeys.list()List this org + environment's keys (no plaintext).
POST/v1/api-keysmailfully.apiKeys.create(input)Mint a key (201; plaintext returned once).
POST/v1/api-keys/{id}/revokemailfully.apiKeys.revoke(id)Revoke a key (idempotent).
POST/v1/api-keys/{id}/rotatemailfully.apiKeys.rotate(id)Mint a successor; grace-revoke the old key.

Webhooks

Register, list, and delete webhook endpoints, and read delivery history. The signing secret is returned exactly once.

MethodPathSDK callSummary
POST/v1/webhooksmailfully.webhooks.create(input)Register an endpoint (201; signing secret returned once).
GET/v1/webhooksmailfully.webhooks.list()List this org's webhooks (no signing secret).
DELETE/v1/webhooks/{id}mailfully.webhooks.delete(id)Delete an endpoint.
GET/v1/webhooks/{id}/deliveriesmailfully.webhooks.deliveries(id, query?)Keyset-paginated delivery history.

Suppressions

List, add, and remove suppressed recipients. Suppressions are org-global.

MethodPathSDK callSummary
GET/v1/suppressionsmailfully.suppressions.list(query?)Keyset-paginated list with optional filters.
POST/v1/suppressionsmailfully.suppressions.create(input)Manually suppress an address (idempotent).
DELETE/v1/suppressions/{id}mailfully.suppressions.delete(id)Remove a suppression.

Analytics

Day-granular delivery rollups plus the external (Gmail Postmaster) reputation signal.

MethodPathSDK callSummary
GET/v1/analytics/dailymailfully.analytics.daily(query?)Daily delivery totals.
GET/v1/analytics/by-domainmailfully.analytics.byDomain(query?)Per-(domain, day) counters.
GET/v1/analytics/by-tagmailfully.analytics.byTag(query?)Per-(tag, day) counters.
GET/v1/analytics/external-reputationmailfully.analytics.externalReputation()Latest Gmail Postmaster spam rate per domain.

Authentication

Authenticate every request with your API key as a bearer token. The SDK sets this header for you from the apiKey you pass to the constructor; it never reads the environment and never logs the key. Keys are scoped to a single environment (test or live) — use a test key for the simulator path and a live key for real sends.

request headerhttp
Authorization: Bearer mf_live_…

Idempotency

The send and batch mutations accept an Idempotency-Key header so a retry never sends twice. Replaying a request with a key the server has already seen returns the original 202 { id } — the same id, with no duplicate send. In the SDK, pass it as the second argument:

idempotent-send.tsts
await mailfully.emails.send(input, { idempotencyKey: `welcome:${userId}` });

Choose a key that is stable for the logical operation — e.g. a key derived from the user and the email type — so a network retry of the same intent collapses to one send.

Errors

The API returns failures as a JSON envelope with a machine-readable type, a human message, and (for validation errors) the offending param.

error responsejson
{
  "error": {
    "type": "validation_error",
    "message": "subject is required",
    "param": "subject"
  }
}

The SDK surfaces this as the error arm of a { data, error } tuple — it never throws. Branch on error to narrow data to non-null on the success path:

handle-error.tsts
const { data, error } = await mailfully.emails.send(input);

// The SDK NEVER throws. On any failure — transport, non-2xx, or a parse
// error — `data` is null and `error` is a MailfullyError carrying the
// envelope fields plus the HTTP status (or null for a transport failure).
if (error) {
  console.error(error.type);       // e.g. "validation_error" (or null)
  console.error(error.message);    // human-readable message
  console.error(error.param);      // offending field (validation only)
  console.error(error.statusCode); // HTTP status, or null (transport)
} else {
  console.log(data.id);            // success — narrowed to non-null
}

Pagination

List endpoints are cursor-paginated. A page is { data, has_more, next_cursor }: when has_more is true, pass next_cursor as the next request’s cursor.

paginate.tsts
let cursor: string | undefined;

do {
  const { data, error } = await mailfully.emails.list({ limit: 100, cursor });
  if (error) throw error; // or handle it — your call
  for (const row of data.data) {
    console.log(row.id, row.last_event);
  }
  cursor = data.next_cursor ?? undefined;
} while (cursor); // `has_more` is true exactly while next_cursor is set

Test mode & the simulator

No domain required

Send with a test-environment API key and every recipient is rewritten to the SES Mailbox Simulator (@simulator.amazonses.com) — no real mail is sent and no verified domain is needed. The recipient’s local-part selects the simulated outcome; any other local-part behaves like success. You can also address bounce@simulator.amazonses.com and friends directly.

Recipient local-partSimulated outcome
success@…Delivered cleanly (also the default).
bounce@…A hard bounce.
complaint@…A spam complaint.
suppressionlist@…A bounce because the address is on the SES suppression list.
ooto@…An out-of-the-office auto-reply.

See the zero-to-send walkthrough for a runnable test send, or the quickstarts for full integration examples.