API overview

Base URL, authentication, error format, idempotency, rate limits, and pagination for the Mailfully API.

The Mailfully API is a REST API for sending transactional email and managing sending domains, templates, webhooks, and suppressions. All requests and responses are JSON over HTTPS.

Base URL

Every route you call lives under the /v1 path prefix. Versioning is path-only: there is no version header.

https://api.mailfully.com

Authentication

Authenticate your requests with a bearer token in the Authorization header. A token starting with mf_ is an API key; any other bearer token is a dashboard session token.

Authorization: Bearer mf_live_xxxxxxxxxxxx

API keys use the format mf_<env>_<random>, where <env> is live or test. Your API key's environment separates message data. A test key never sees live messages, events, or analytics, and vice versa. Templates, domains, webhooks, and suppressions are org-wide: both environments share them.

Your access is scoped. There are seven scopes total: you can mint four onto an API key (send, read:emails, read:analytics, read:suppressions); the other three are session-only and can never be granted to an API key (manage:webhooks, manage:suppressions, manage:keys). Call operations that require a manage:* scope with a dashboard session token (an API key gets 403 every time).

See Authentication for the full scope table and dashboard-session details.

Content type

Send your request bodies as application/json and expect application/json responses. Set Content-Type: application/json on every request with a body.

Errors

Every error response you receive uses the same envelope:

{
  "error": {
    "type": "validation_error",
    "message": "The request was invalid.",
    "param": "to"
  }
}

param is present only on validation errors and names the offending field. For batch sends, the index is prefixed onto the field, for example [37].to.

StatusRepresentative type
400invalid_idempotency_key
401missing_api_key, invalid_api_key, invalid_session_token
403insufficient_scope, forbidden, unverified_domain_required, invalid_api_key
404not_found
409conflict, invalid_idempotent_request, concurrent_idempotent_requests
422validation_error
429rate_limit_exceeded, daily_quota_exceeded, monthly_quota_exceeded, spend_cap_exceeded
500internal_server_error
503service_unavailable

Validation failures return 422, not 400.

See Errors for the full catalog of error types and messages.

Idempotency

Send an Idempotency-Key header on POST /v1/emails or POST /v1/emails/batch to make your retries safe:

Idempotency-Key: order-8412-shipped

A repeated key with the same request body replays the original response for 24 hours; the same key with a different body returns 409. Idempotency is opt-in — no header means no deduplication. It is honored only on those two endpoints.

See Idempotency for replay semantics and edge cases.

Rate limits

Rate limiting applies to POST /v1/emails and POST /v1/emails/batch. Your organization gets a token bucket with a burst of 10 requests and a steady refill of 8 requests per second.

Every allowed request on those endpoints carries:

ratelimit-limit: 10
ratelimit-remaining: 7
ratelimit-reset: 0

If you're rate-limited, you get 429 rate_limit_exceeded with a retry-after header (seconds) alongside the ratelimit-* headers.

See Rate limits for bucket details and backoff guidance.

Pagination

Four lists paginate: GET /v1/emails, GET /v1/suppressions, GET /v1/webhooks/{id}/deliveries, and GET /v1/webhook-dead-letters. They take limit and cursor query parameters and return a data array with has_more and next_cursor. limit defaults to 25 (max 100); webhook deliveries and dead-letter lists default to 50 (also max 100). cursor is opaque. Pass back the previous response's next_cursor verbatim; never construct your own.

The short lists (API keys, webhooks, templates, domains) are not paginated: they return everything in data with no cursor fields.

{
  "data": [
    { "id": "01JZFYK2N8Q4T7W9E1R3M5X0AB", "...": "..." }
  ],
  "has_more": true,
  "next_cursor": "eyJ0cyI6IjIwMjYtMDctMDggMTQ6MDI6MTEiLCJpZCI6IjAxSlpGWUsyTjhRNFQ3VzlFMVIzTTVYMEFCIn0"
}

When has_more is false, next_cursor is null.