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.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| POST | /v1/emails | mailfully.emails.send(input, opts?) | Send one email (202 → { id }). |
| POST | /v1/emails/batch | mailfully.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/emails | mailfully.emails.list(query?) | The keyset-paginated message log with filters. |
| POST | /v1/emails/{id}/cancel | mailfully.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}/events | mailfully.emails.events(id) | The per-message event timeline. |
Domains
Add and verify custom sending domains and toggle open/click tracking.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| POST | /v1/domains | mailfully.domains.create(input) | Add a sending domain (201 → domain + DNS records). |
| GET | /v1/domains | mailfully.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}/verify | mailfully.domains.verify(id) | Poll SES and persist the new verification status. |
| POST | /v1/domains/{id}/tracking | mailfully.domains.tracking(id, input) | Toggle open/click tracking. |
API keys
Mint, list, revoke, and rotate API keys. The plaintext key is returned exactly once.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| GET | /v1/api-keys | mailfully.apiKeys.list() | List this org + environment's keys (no plaintext). |
| POST | /v1/api-keys | mailfully.apiKeys.create(input) | Mint a key (201; plaintext returned once). |
| POST | /v1/api-keys/{id}/revoke | mailfully.apiKeys.revoke(id) | Revoke a key (idempotent). |
| POST | /v1/api-keys/{id}/rotate | mailfully.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.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| POST | /v1/webhooks | mailfully.webhooks.create(input) | Register an endpoint (201; signing secret returned once). |
| GET | /v1/webhooks | mailfully.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}/deliveries | mailfully.webhooks.deliveries(id, query?) | Keyset-paginated delivery history. |
Suppressions
List, add, and remove suppressed recipients. Suppressions are org-global.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| GET | /v1/suppressions | mailfully.suppressions.list(query?) | Keyset-paginated list with optional filters. |
| POST | /v1/suppressions | mailfully.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.
| Method | Path | SDK call | Summary |
|---|---|---|---|
| GET | /v1/analytics/daily | mailfully.analytics.daily(query?) | Daily delivery totals. |
| GET | /v1/analytics/by-domain | mailfully.analytics.byDomain(query?) | Per-(domain, day) counters. |
| GET | /v1/analytics/by-tag | mailfully.analytics.byTag(query?) | Per-(tag, day) counters. |
| GET | /v1/analytics/external-reputation | mailfully.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.
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:
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": {
"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:
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.
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 setTest mode & the simulator
No domain requiredSend 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 [email protected] and friends directly.
| Recipient local-part | Simulated 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.