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.
dry_run: truewrites nothing and returns a per-field diff of old and new value, confidence and source. Tune a schema against real historical mail without touching it.redeliverdefaults to false. Re-parsing five thousand messages does not fire five thousand webhooks at your production endpoint. Fixing your data and re-notifying your downstream are separate decisions.- It returns a job id immediately; poll it rather than holding the request open.
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
| Plan | Emails / month | Original bytes kept | Attachment bytes kept |
|---|---|---|---|
| Free — free | 300 | 30 days | 7 days |
| Starter — $9/mo | 5,000 | 90 days | 30 days |
| Pro — $29/mo | 25,000 | 180 days | 60 days |
| Scale — $99/mo | 150,000 | 365 days | 90 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.