REST API

Base URL https://api.foundermail.mx. Requests and responses are JSON. Authenticate every request with an API key from Settings → API keys, sent in the X-Api-Key header. API keys are on every plan, the Free one included.

curl -H "X-Api-Key: $FOUNDERMAIL_KEY" https://api.foundermail.mx/mailboxes

Your first requests

An API key is for your own code: a script, a backend job or an automation tool that reads the mail arriving at your mailboxes, waits for a particular message, drafts or sends a reply, or attaches your customer records to contacts. If you only want to read and answer mail, you do not need one: the dashboard and any IMAP client do that with the mailbox's own address and password. To use FounderMail from Claude, Cursor or another assistant, the same key goes into the MCP server instead.

  1. Create a key under Settings → API keys and tick what it may do. The default, reading mail and writing drafts, is enough to try everything below except sending.
  2. Keep the key on a server or in an environment variable. It acts for your whole workspace, so never put it in a web page, a mobile app or a public repository.
  3. Check that it works by listing your mailboxes. A 401 means the key was not sent or is wrong; a 403 names the scope the key lacks.
export FOUNDERMAIL_KEY="paste-your-key-here"

# Which mailboxes can this key see?
curl -H "X-Api-Key: $FOUNDERMAIL_KEY" https://api.foundermail.mx/mailboxes

# The ten newest messages in hello@yourdomain.com
curl -H "X-Api-Key: $FOUNDERMAIL_KEY" \
  "https://api.foundermail.mx/messages/hello@yourdomain.com?take=10"

# Wait up to 60 seconds for the next message to arrive there
curl -H "X-Api-Key: $FOUNDERMAIL_KEY" \
  "https://api.foundermail.mx/messages/hello@yourdomain.com/wait?timeout=60"

On Windows PowerShell the first call is:

$key = "paste-your-key-here"
Invoke-RestMethod https://api.foundermail.mx/mailboxes -Headers @{ "X-Api-Key" = $key }

In code it is the same header on an ordinary HTTPS request. In JavaScript, for example:

const res = await fetch("https://api.foundermail.mx/messages/hello@yourdomain.com?take=10", {
  headers: { "X-Api-Key": process.env.FOUNDERMAIL_KEY },
});
const messages = await res.json();

To send from code, create a key with mail:send ticked and see Sending; to put a person between your code and the recipient, use Drafts instead.

The API can send, and it can draft. A key with mail:send sends mail from a mailbox; a key with only mail:draft leaves the message in Drafts for a person to send, which is what you want when a human should read it first. Sending through the API goes through the same gate as the dashboard's compose window, so the same rules apply: most domains may only reply to people who have written to them, and every send counts against the plan's allowance.

Scopes

A key carries a set of scopes, chosen when you create it in the dashboard — a new key reads mail and writes drafts unless you say otherwise, and sending is never on by default. Keys made before scopes could be chosen keep every scope, and the dashboard labels them Full access (legacy); replace one to narrow it. The names matter when you read a 403, which names the scope that was missing.

ScopeGrants
inbox:readList mailboxes; read and wait for messages
inbox:writeCreate and delete mailboxes
mail:draftCreate, list and delete drafts. Never sends
mail:sendSend mail from a mailbox
config:writeTemplates and contact links
lists:readRead mailing lists and their subscribers (Campaigns)
lists:writeAsk people to join a list, and take them off. Cannot send a campaign
adminAll of the above

Mailboxes

A mailbox is addressed by its key, the local part of its address. A bare key such as hello works while exactly one mailbox in the workspace has it; when the same key exists on two domains, use the full address, hello@yourdomain.com.

EndpointScope
GET /mailboxesinbox:readEvery mailbox in the workspace
GET /mailboxes/{key}inbox:readOne mailbox
POST /mailboxesinbox:writeCreate a mailbox
DELETE /mailboxes/{key}inbox:writeDelete a mailbox and its messages
POST /mailboxes
{ "name": "orders", "domain": "yourdomain.com" }

200 OK
{
  "key": "orders",
  "domain": "yourdomain.com",
  "address": "orders@yourdomain.com",
  "password": "…",
  "smtpHost": "smtp.foundermail.mx", "smtpPort": 587,
  "imapHost": "imap.foundermail.mx", "imapPort": 993
}

domain is required and must be one of your domains. If name is omitted a random key is generated, which is useful for an integration that needs an address without inventing one. The password is the mailbox's IMAP and SMTP password.

Messages

Endpoint
GET /messages/{key}?skip=0&take=10Messages in a mailbox, newest first. take is capped at 25. Add since=2026-10-10T08:00:00Z to list only what arrived at or after that time.
GET /messages/{key}/countHow many messages the mailbox holds
GET /messages/{key}/lastThe most recent message
GET /messages/{key}/singleThe only message in the mailbox; an error if there are none or several
GET /messages/{key}/get?id={uniqueId}One message by its unique id
GET /messages/{key}/wait?timeout=60&since=…Long-poll: returns the first message received after since (default: now), or 204 when timeout seconds pass. Default 60, maximum 120.
{
  "id": 48211,
  "uniqueId": "9d0b6b1e-…",
  "from": ["dana@arboretum.co"],
  "to": ["billing@yourdomain.com"],
  "cc": [], "bcc": [],
  "subject": "Invoice 2291 shows the old VAT rate",
  "body": "…",
  "isHtml": true,
  "received": "2026-09-05T07:14:02Z",
  "spamAction": "Deliver",
  "spamScore": 0.8,
  "spamSymbols": "HFILTER_HOSTNAME_UNKNOWN(2.5)"
}

The list endpoint and the message endpoints return the message's parsed body: HTML when the message had one, otherwise its text. Attachments are not exposed through the API.

spamAction is what the filter decided — Deliver, Junk, Defer or Reject — and spamSymbols lists the rules that fired, heaviest first, which is the answer to "why did this land in Junk". All three are null when nothing scanned the message: no filter reachable, or the message too large to scan. That is not the same as a clean verdict, and the difference is worth checking if you are acting on the field.

Parsed with a template

Add a template key to last or single, or pass template= to wait, and the message comes back with the values the template extracted instead of the raw subject and body. See Templates.

GET /messages/orders/order-confirmation/last

{
  "uniqueId": "…",
  "from": ["shop@example.com"], "to": ["orders@yourdomain.com"],
  "subject": { "order": "1048" },
  "body":    { "total": "€240.00" },
  "isHtml": false,
  "received": "2026-09-05T07:14:02Z"
}

Drafts

A draft written through the API is the same row as one written in the dashboard: it appears in the mailbox's Drafts folder, in the dashboard's Drafts view and in any IMAP client, from where a person sends it.

Endpoint
POST /messages/{key}/draftCreate a draft, or overwrite one when the body carries its id
GET /messages/{key}/draftsDrafts in the mailbox
DELETE /messages/{key}/drafts/{id}Delete one
POST /messages/billing/draft
{
  "to": ["dana@arboretum.co"],
  "subject": "Re: Invoice 2291 shows the old VAT rate",
  "text": "Hi Dana, a corrected invoice is attached…",
  "html": null,
  "id": null
}

html takes precedence over text when both are given.

Sending

POST /messages/{key}/send sends now, from that mailbox's own address, and needs the mail:send scope. The message is recorded in Sent exactly as one sent from the dashboard is, so it appears in the thread, in the dashboard and in any IMAP client.

POST /messages/billing/send
{
  "to": ["dana@arboretum.co"],
  "subject": "Re: Invoice 2291 shows the old VAT rate",
  "text": "Hi Dana, a corrected invoice is attached…",
  "html": null,
  "cc": [],
  "bcc": [],
  "inReplyToMessageId": 48211
}

inReplyToMessageId is the id of the message being answered, as the read endpoints return it. Supply it unless the sending domain has been approved for writing to people who have not written first: without it such a send is refused with 403, which is the anti-abuse rule working rather than a fault. A 202 means the delivery provider has accepted the message.

StatusMeans
202Accepted for delivery; the body carries the Sent message's id
403The domain may only reply, is not verified for sending, or the mailbox is disabled
429The monthly allowance, or the Free plan's daily one, is used up
503Delivery is unavailable; nothing was sent

There is no bulk endpoint and there will not be one: FounderMail hosts a team's mail, and transactional or marketing blasts belong with a service built for them.

Contacts

Attach your own record of a customer to the contact FounderMail keeps for their address, so it appears beside their mail in the dashboard.

PUT /contacts/{email}/link
{
  "externalUserId": "usr_8Kd21",
  "plan": "Pro",
  "monthlyRevenue": 290,
  "signedUpAt": "2025-03-14T09:00:00Z",
  "displayName": "Dana Kessler",
  "attributes": "{\"Seats\":\"12\"}"
}

DELETE /contacts/{email}/link

Every field is optional. attributes is a JSON object serialised as a string; its members are shown as extra rows under In your app. The response includes created, true when the address had never written to you and the contact was created by this call. Such a contact is not verified by inbound mail and does not make the address a permitted recipient in reply-only mode; see Contacts.

Templates

Endpoint
GET /templatesAll templates
GET /templates/{key}One template
POST /templatesCreate: { "key", "subject", "body" }
PUT /templates/{key}Update
DELETE /templates/{key}Delete
POST /templates/testTry patterns against sample text without saving: { "subject", "body", "sampleSubject", "sampleBody" }

Status

GET /status returns 200 when the API is up. It requires a key like everything else.

Errors and limits

  • 401: missing or unknown key.
  • 403 with a missingScope field: the key lacks that scope. Check the key before checking the data.
  • 404: the mailbox, message or template is not in your workspace. Ids from another workspace are not found rather than forbidden.
  • 429: the plan's request rate was exceeded (Free: 20 a minute, 100 an hour; Plus: 40 a minute, 300 an hour; Pro: 60 a minute, 1,500 an hour; Scale: 200 a minute, 10,000 an hour). Back off and retry.
  • Other errors are returned as problem details with a title and detail.
Say hello

Say hello

A question before you sign up, or something that is not working: write it here and a person answers by email.

A person answers by email, usually within a day.