API reference

Every number on this page is read from the running configuration, so it says what this deployment actually enforces. The written guides are at /docs.

Base URL https://mailmint.app.mintapis.com. Every call carries Authorization: Bearer mm_live_….

Authentication

Keys are shown once, at creation. A key beginning mm_test_ behaves identically but is never counted against your quota — use it in CI.

Mailboxes

POST   /v1/mailboxes      {name, schema?, webhook_url?}
GET    /v1/mailboxes
GET    /v1/mailboxes/:id
PATCH  /v1/mailboxes/:id  {name?, schema?, webhook_url?, webhook_secret?}
DELETE /v1/mailboxes/:id

Each mailbox has an address <token>@smooth-operator.online. slug.token@ and token+tag@ reach the same mailbox.

Schema

{ "name": "total", "type": "number", "description": "grand total incl. tax",
  "required": true, "hint": "labelled Total or Amount Due" }

Types: string, number, integer, boolean, date, datetime, email, url, phone, currency, enum, array, object. enum needs options; array needs items; object needs fields.

Messages

GET  /v1/messages?mailbox_id=&since=&cursor=&limit=&status=
                 &needs_review=true&flag=arithmetic_mismatch&view=review
GET  /v1/messages/:id?include=attachments,extracted_text&exclude=extracted
GET  /v1/messages/:id/raw
GET  /v1/attachments/:id
POST /v1/messages/:id/reparse  {schema?, schema_version?, deliver?}

needs_review=true and flag= are indexed, so the review queue stays fast as a mailbox fills up. view=review adds, per message, which flag fired on which field and the evidence string the value came from.

include=attachments inlines the file bytes as base64. An attachment that was read by the extractor carries extracted: {kind, text, pages, tables, meta}; its text is truncated to 2000 characters unless you ask for include=extracted_text, and exclude=extracted drops it entirely.

Finding what went wrong

Every field carries confidence, source and evidence — the verbatim substring of the message the value was taken from. A value whose evidence is not actually in the message is flagged hallucinated_evidence and its confidence is halved. A field where the rule layer and the model disagreed keeps the rule's answer and is flagged rule_llm_disagreement. Invoices whose line items do not add up to the total are flagged arithmetic_mismatch.

needs_review is true when any of those fired. The review queue is the same query with a page around it.

Re-parsing old mail

POST /v1/mailboxes/:id/reparse
  {since?, until?, limit?, status?, needs_review?, flag?,
   schema?, schema_version?, dry_run?, redeliver?}     -> 202 {job_id, poll}
GET  /v1/reparse/:job_id   -> {status, done, total, changed, diffs:[…]}

Runs the parser again over stored messages, from the ORIGINAL raw bytes. This is what a layout change from one of your senders is fixed with, weeks after it happened.

How far back it reaches is how long the original bytes are kept — see retention below.

Webhooks

POST   /v1/mailboxes/:id/webhooks   {url, description?}  -> {id, url, secret}
GET    /v1/mailboxes/:id/webhooks
GET    /v1/webhooks/:id
PATCH  /v1/webhooks/:id             {url?, description?, active?, secret?}
DELETE /v1/webhooks/:id

A mailbox has many endpoints, each with its own signing secret. Two workflows can watch one mailbox without touching each other: adding, pausing, rotating or deleting one endpoint never affects another. mailbox.webhook_url still works and is an alias for the first endpoint.

Headers: x-mailmint-event: message.parsed, x-mailmint-delivery: dlv_…, x-mailmint-endpoint: whe_…, x-mailmint-signature: t=1788739142,v1=104831db…

const [t, v1] = header.split(',').map(p => p.split('=')[1]);
const expected = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected,'hex'), Buffer.from(v1,'hex'))) reject();
if (Math.abs(Date.now()/1000 - Number(t)) > 300) reject();   // replay window

Retries at 0s, 30s, 2m, 10m, 1h, 6h. 10s timeout. A 4xx other than 408 or 429 is not retried. An endpoint whose deliveries exhaust their retries 10 times in a row is switched off and flagged on the dashboard, rather than retried forever into a dead host.

Sender authentication

auth carries spf, dkim, dmarc and spam_score; auth_details carries the finer result when the receiving edge produced one. dkim is one of pass, fail, body_altered, none, temperror, permerror.

body_altered is not a failure. The signature and key are genuine and the body changed after signing — which is what forwarding, mailing lists and corporate security gateways do routinely. It raises dkim_body_altered, never auth_fail:dkim, and never sets needs_review. Only a real fail does.

spf: "none" means it could not be checked, not that it was checked and found nothing — mail arriving through Cloudflare Email Routing gives the receiving worker no client IP, so there is nothing to evaluate. It is not treated as a problem anywhere.

Quota

The free plan is 300 parsed emails a month, no card. Over the line, mail is still received, stored, listed and delivered — it is flagged quota_exceeded, and only the extraction pass is skipped. Your mail is never bounced, and nothing you have already received is taken away. Re-parsing something you have already paid for is free.

Retention & limits

PlanEmails / monthOriginal bytes keptAttachment bytes kept
Free — free 30030 days7 days
Starter — $9/mo 5,00090 days30 days
Pro — $29/mo 25,000180 days60 days
Scale — $99/mo 150,000365 days90 days

The original bytes column is how far back reparse can reach, because a re-parse replays them. Attachment bytes expire sooner: they are the bulk of the storage and are not needed to re-parse a message body. Events are kept 7 days.

Messages over 25 MB are recorded but not stored raw; attachments over 10 MB are recorded but their bytes are not. Both cases are flagged, never silently dropped.

Rate limits

240 API requests a minute per account, burst 40. Inbound mail is not rate limited.