Integrations

Using the API

Read conversations, set up domains, and send mail over HTTP, with the endpoints, examples, and errors you will meet.

The MailMoose API lets you read conversations and send mail from your own scripts and tools. Every request carries an API key and acts as the person who created it.

The base path is /api/v1. All requests and responses are JSON.

Creating domains or addresses needs room in your plan: the free plan includes one domain with two addresses, the paid plan is unlimited. Sending also requires sender readiness and quota. Reading existing accessible mail is not blocked merely because a subscription expires.

Authenticate

Send your key as a bearer token:

Authorization: Bearer mm_live_your_key_here

Check that a key works before doing anything else:

curl https://your-mailmoose-url/api/v1/me \
  -H "Authorization: Bearer mm_live_your_key_here"

The response tells you which permissions the key has and which addresses it can send from.

/me requires mail:read. For a send-only app key, validate it without reading mail or sending a test:

GET /api/v1/senders
Authorization: Bearer mm_live_your_key_here

This returns the key's scopes, its daily limit and its accessible, sending-ready sender addresses. It does not guarantee delivery or reserve quota; billing and limits are checked when sending.

Automatic emails from your app

Your coding agent can prepare a project key through MCP. Store these on your server, never in browser code:

MAILMOOSE_API_URL=https://www.mailmoose.dev/api/v1
MAILMOOSE_API_KEY=your_new_project_key
MAILMOOSE_FROM=hello@your-verified-domain.com

MAILMOOSE_API_URL includes /api/v1; append /messages, not another /api/v1.

// Server-side code only. Call from an approved, authenticated app event.
export async function sendWelcomeEmail(verifiedUserEmail: string) {
  const apiUrl = process.env.MAILMOOSE_API_URL;
  const apiKey = process.env.MAILMOOSE_API_KEY;
  const from = process.env.MAILMOOSE_FROM;
  if (!apiUrl || !apiKey || !from) throw new Error("MailMoose is not configured");

  const response = await fetch(`${apiUrl}/messages`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      from,
      to: [verifiedUserEmail],
      subject: "Welcome to Learn Dari",
      body: "Your first lesson is ready. Welcome aboard!",
      campaign: "welcome_email",
      // Only enable after the owner approves this transactional flow.
      // Derive the address from the app's verified user, never incoming mail.
      confirmNewRecipient: true,
    }),
  });
  if (!response.ok) throw new Error(`MailMoose request failed (${response.status})`);
  return response.json();
}

The app sends automatically over HTTP when its event fires. MCP helps your agent configure and build the integration; no AI call is needed at runtime. Require owner approval before activating first-contact transactional sends or sending a real test. Signup, payment and webhook triggers must be authenticated and deduplicated in your app. Do not automatically retry a timed-out send: delivery may already have happened; check MailMoose Sent before retrying. Keep secrets out of error output, analytics and Git, and rotate expiring keys before they expire.

Read conversations

List the most recent conversations:

GET /api/v1/threads

Accepts limit, unread=true, address to restrict to one mailbox, and q to search subjects, senders, and message text.

Only conversations you have received mail in are listed. Mail you sent that nobody has answered is not a conversation yet, and an app that sends a lot of it would bury everything that is. Pass include=all to list those too.

Read a single conversation with all its messages:

GET /api/v1/threads/{id}

Each message includes its authentication result. A message that failed DKIM or DMARC is far more likely to be forged, so check it before acting on anything financial.

Reply to a conversation

POST /api/v1/threads/{id}/reply
{ "body": "Thursday works for us." }

Notice there is no recipient. MailMoose addresses the reply using the conversation's own records, so a reply cannot be sent somewhere the conversation did not come from. Pass "replyAll": true to include everyone on the original message.

Send a new message

POST /api/v1/messages
{
  "from": "thomas@yourdomain.com",
  "to": ["jane@example.com"],
  "subject": "Quote for January",
  "body": "Hi Jane, here is the quote we discussed."
}

If you have never corresponded with a recipient, this returns 409 with the code confirmation_required and names the unfamiliar addresses. Repeat the request with "confirmNewRecipient": true to send anyway.

That step exists so an automated caller cannot quietly start mailing strangers.

Set up a domain

Connect a domain you own. The response carries the DNS records still to add and, for each one, whether to add it, edit what is already there, or replace it:

POST /api/v1/domains
{ "name": "yourdomain.com", "label": "Your business" }

A domain this organization already has answers 200 with "created": false and its current setup, rather than an error, so a retry after a dropped response is harmless.

Check it again once the records are in place:

GET /api/v1/domains/{name}

This asks public DNS and the mail provider on every call, so it takes a few seconds. It is for after a change, not for polling.

Receiving and sending verify separately. Connecting a domain sets up receiving; turn on sending for it with:

POST /api/v1/domains/{name}/sending

That answers with the same setup shape, now including the records that authorise sending. Safe to repeat: it re-syncs rather than registering the domain twice, and asks the sending provider to check again.

All three need the Manage domains permission, and connecting is limited to owners and admins.

Manage addresses

GET  /api/v1/addresses
POST /api/v1/addresses
{ "localPart": "hello", "domain": "yourdomain.com" }

Creating an address needs the Manage domains permission, an owner/admin role, room in your plan (the free plan includes two addresses) and sufficient domain verification. GET /api/v1/domains/{name} returns the current setup state, including whether addresses can be created. A verifying domain can allow preparing inboxes before switching MX.

App email templates

Open Templates → App templates to see your latest sent email designs, with or without campaign tags. Filter by domain to find its welcome emails, receipts and other automated messages.

Include a stable campaign value when sending through the API, for example "campaign": "welcome". MailMoose groups sends with that tag within the domain. HTML emails without a campaign tag also appear, grouped by domain and subject. Queued or failed sends do not appear as used templates.

These are read-only sent samples, not saved editable templates. Update the content in the app that sends them. Only mailboxes you can access appear. HTTPS images load in the sandboxed previews; scripts and forms remain blocked. MailMoose emails shows the actual designs for account notifications separately; it does not indicate which notifications have been delivered.

Sending limits

The paid plan includes 500 sending recipients per billing month, shared across all domains, inboxes and keys. Extra recipients use prepaid credit at $0.003 each; with no credit left, sending stops until more is added. The free plan allows 50 per rolling 30 days instead. API keys also default to a 100-message rolling 24-hour safety limit, which never adds to the account's allowance.

GET /api/v1/me reports usage under sending: on the paid plan used, included, prepaidRecipients, periodStart and periodEnd; on the free plan used, limit and windowDays. sendingPaused is true while sending is paused for review, after a spam complaint, a high bounce rate or a disputed payment. How limits work.

These defaults may be overridden. API sends are immediate and do not use the web composer’s 10-second undo window.

Errors

Every error has the same shape:

{ "error": { "code": "insufficient_scope", "message": "..." } }
StatusCodeWhat to do
401unauthorizedThe key is wrong, revoked, or expired. Create a new one.
402payment_requiredThe free plan's limits are reached, or paid access or prepaid credit has run out. Upgrade, renew or add prepaid credit in Billing.
403insufficient_scopeThe key lacks the permission this endpoint needs.
404not_foundNo such conversation, or it belongs to somebody else.
409confirmation_requiredResend with confirmNewRecipient set to true.
429rate_limitedThe key or organization send limit is reached. Wait for older sends to leave the 24-hour window.
400bad_requestSomething in the request body is wrong. The message says what.

A conversation belonging to another account returns 404 rather than 403, so identifiers cannot be used to discover what exists.

Related

Last updated 2026-10-31