Inbound email API · n8n node
An email address in. Structured JSON out.
MailMint turns an incoming email into the fields you asked for — each one with a confidence we compute, the layer it came from, and the exact text of the message it was read out of. Delivered by webhook, by polling, or into n8n.
- 300 parsed emails a month free
- No card to sign up
POST /v1/parsestores nothing
The emailtext/plain
- From
- example-shop.com <auto-confirm@example-shop.com>
- Subject
- Your Example-Shop.com order of Anker 737 Power Bank...
- Date
- Tue, 19 Aug 2026 21:14:52 +0000
Hello Florian, Thank you for your order. We'll send a confirmation when your item ships. Order # 114-7729183-4462618 Order Date: August 19, 2026 Arriving: Friday, August 22, 2026 Anker 737 Power Bank (PowerCore 24K) $109.99 Qty: 1 Sold by: AnkerDirect Item Subtotal: $109.99 Shipping & Handling: $0.00 Total Before Tax: $109.99 Estimated Tax: $9.35 Order Total: $119.34
What MailMint returnsapplication/json
{
"fields": {
"order_number": { "value": "114-7729183-4462618",
"confidence": 0.97, "source": "rule+llm",
"evidence": "Order # 114-7729183-4462618" },
"order_date": { "value": "2026-08-19",
"confidence": 0.97, "source": "rule+llm",
"evidence": "Order Date: August 19, 2026" },
"arriving": { "value": "2026-08-22",
"confidence": 0.97, "source": "rule+llm",
"evidence": "Arriving: Friday, August 22, 2026" },
"total": { "value": { "amount": 119.34, "currency": "USD" },
"confidence": 0.97, "source": "rule+llm",
"evidence": "Total: $119.34" },
"line_items": { "value": [
{ "description": "Anker 737 Power Bank (PowerCore 24K)",
"qty": 1, "amount": 109.99 } ],
"confidence": 0.97, "source": "rule+llm",
"evidence": "anker 737 power bank (powercore 24k) $109.99" }
},
"flags": [],
"needs_review": false
}
packages/parser, the code behind POST /v1/parse) run on 25 September 2026
against fx-02-order-confirmation.eml from its test corpus, with a five-field schema. The model
that answered was deepseek-ai/DeepSeek-V4-Flash-0731-TEE. The full response also carries the
decoded headers, the body, attachments, tables, sender authentication and timings; it is trimmed here to
the schema’s answers. Highlighted: the evidence each value was read from.88 % / 95 %
Precision and recall on a hold-out set the parser had never seen: 36 adversarial messages, 163 labelled field slots.
91.6 %
Of field slots resolved by deterministic rules before any model runs. Those values are byte-identical run to run.
69 / 69
Genuine third-party DKIM signatures that verify against live DNS, from gmail.com, Fastmail, Pobox, GMX and others.
0
Competitors that return a per-field confidence or evidence span — checked across 363 vendor help articles and two API schemas.
Hold-out figures measured 31 August 2026, three runs (87.1–87.8 % and
91.5–93.6 %). On our own labelled corpus the pipeline scores 100 %, and we do not quote that,
because a number measured on the cases you tuned against is not a number. 36 unseen messages is a small
sample, not a general accuracy guarantee. The harness and every case it gets wrong are in
packages/parser/.
How it works
Three steps, and no template to click together
You describe the fields in words. There is no sample to upload, no region to draw and no positional rule that breaks when a sender changes their layout.
-
Describe the fields
A name, one of 13 types and a sentence of description per field —
string,date,currency,enum, typedarrays of objects and more. Change it over the API at any time.{ "name": "total", "type": "currency", "description": "grand total incl. tax" } -
Send the mail
Forward it to your mailbox’s own address on
@smooth-operator.online, post raw MIME to/v1/parse, or drop the MailMint node after n8n’s IMAP or Gmail trigger.k7m2xq4h9bwz@smooth-operator.online invoices.k7m2xq4h9bwz@… # slug alias k7m2xq4h9bwz+acme@… # +tag survives
-
Get JSON you can check
A signed webhook, a polling feed or the n8n trigger. Route on one boolean:
needs_reviewis true whenever any field is doubtful, missing or unsupported by the text.x-mailmint-signature: t=1787648043,v1=6a1f… { "id": "msg_01JQ8Z…", "needs_review": false, "fields": { … } }
Confidence & evidence
When it gets something wrong, it says so
“How do I find out that it went wrong?” is the question the n8n, Make and Zapier
threads keep asking. Every value MailMint returns carries four keys — value,
confidence, source and evidence — so the answer is in the
payload.
- Computed, never taken from the model. A model will report 0.95 on a value it invented, so its self-report may only ever lower a score. The ceiling comes from signals that can be checked.
-
The evidence check runs. If the quoted span is not really in the message, the score is capped
at 0.3 and the field is flagged
hallucinated_evidence:<field>. -
Two extractors, independently. Deterministic rules and the model both run. Agreement earns
"source": "rule+llm"; disagreement keeps the rule’s value, lowers the score and raisesrule_llm_disagreement:<field>. -
The arithmetic has to reconcile. Line items against the subtotal, subtotal + tax + shipping
− discount against the total. Failing sets
arithmetic_mismatch. -
Missing is
null. Never invented, never “N/A”, never a default you chose. A required field that is absent raisesmissing_required:<field>. The full rule.
evidence Your receipt from Northwind Analytics #2451-8827
evidence Paid August 21, 2026
evidence Total $82.08
A summary row was read as a product, so the rows sum to 82.08 instead of the 76.00 subtotal.
Also real output, run the same day on another corpus receipt — picked because it
went wrong. The parser took the “Sales tax” line for a line item. It did not hide that: the
arithmetic failed, the row was named as suspect, the field fell to 0.42 and the message came back with
needs_review: true. That is the behaviour you are paying for.
What else is in the box
The unglamorous half, done properly
“We have an LLM” is not a product. The value is the part nobody wants to write: real MIME turned into clean text, tables that keep every row, and an honest answer about who sent it.
Forty rows in, forty rows out
Declare "type": "array" of objects and get a real nested array. Rows are found three ways
— HTML grid, repeating DOM blocks, aligned text — and reconciled. row_count and
truncated ride on every table; 1, 40 and 520-row tables come out complete.
Re-parse mail you already received
The original RFC822 bytes are kept, so a schema fix can be dry-run over last month’s real mail
("dry_run": true) and then applied. Re-delivery is a separate switch, so tuning never fires a
month of webhooks at you. Every result carries parse.schema_version.
Attachments you can address
Each file is its own object with inline and content_id — the pair that
separates the invoice PDF from the signature logo. GET /v1/attachments/:id serves the bytes.
25 MB inbound, 10 MB per stored attachment.
SPF, DKIM and DMARC, for real
Evaluated against live DNS, not copied from someone else’s header. A forward that broke the body
hash is dkim: "body_altered", not fail. Where a result genuinely cannot be
computed it says none instead of a pass we did not earn.
Clean text, reply chain removed
Quoted-printable decoded, legacy charsets normalised to UTF-8, RFC 2047 and 2231 resolved. You get the
raw text, the HTML, text rendered from HTML, and body.stripped_text with the quoted thread and
signature taken off.
Signed webhooks that retry
hmac_sha256(secret, t + "." + rawBody) with the timestamp inside the signature. Six attempts at
0 s, 30 s, 2 m, 10 m, 1 h and 6 h, a stable delivery id for idempotency, and
several endpoints per mailbox, each with its own secret.
Built, not yet wired up — said plainly
MailMint does not read text out of a PDF attachment yet. The extractor exists
(packages/docs: PDF text and tables, spreadsheets, DOCX, OCR fallback) and the response has the
slot for it, attachments[].extracted, but nothing populates that slot today. A message whose
data lives only inside the PDF gives you the file, its metadata and its checksum, not its contents.
The API
One request to try it. No address, no DNS.
POST /v1/parse takes raw MIME — or just a subject and a body — plus
your schema, and returns the same object a mailbox would deliver. Nothing is stored.
- Bearer key, JSON in, JSON out. Machine-readable reference at /openapi.json (OpenAPI 3.1).
-
Mailboxes when you are ready. Create one, get an address, set a
webhook_urlor read the/v1/eventsfeed. -
Filter on doubt.
GET /v1/messages?needs_review=trueis a review queue with no extra code.
# Stateless. No address, no DNS, nothing stored.
curl -X POST $MAILMINT_URL/v1/parse \
-H "Authorization: Bearer $MAILMINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"subject": "Invoice INV-2291 from Acme Ltd",
"text": "Invoice INV-2291\n\nTotal: $31.50\nDue: Sep 8, 2026\n",
"schema": [
{ "name": "invoice_number", "type": "string",
"description": "the invoice or reference number" },
{ "name": "total", "type": "number",
"description": "grand total including tax" },
{ "name": "due_date", "type": "date",
"description": "when payment is due" }
]
}'import { readFile } from 'node:fs/promises';
const raw = await readFile('message.eml');
const res = await fetch(`${process.env.MAILMINT_URL}/v1/parse`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MAILMINT_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
raw_mime: raw.toString('base64'),
schema: [
{ name: 'invoice_number', type: 'string' },
{ name: 'total', type: 'number' },
],
}),
});
const result = await res.json();
if (result.needs_review) console.warn('check this one:', result.flags);
console.log(result.fields.total.value, result.fields.total.confidence);import base64, os, pathlib, requests
raw = pathlib.Path("message.eml").read_bytes()
res = requests.post(
f"{os.environ['MAILMINT_URL']}/v1/parse",
headers={"Authorization": f"Bearer {os.environ['MAILMINT_API_KEY']}"},
json={
"raw_mime": base64.b64encode(raw).decode(),
"schema": [
{"name": "invoice_number", "type": "string"},
{"name": "total", "type": "number"},
],
},
timeout=60,
)
res.raise_for_status()
result = res.json()
if result["needs_review"]:
print("check this one:", result["flags"])
total = result["fields"]["total"]
print(total["value"], total["confidence"], total["evidence"])# Mail sent to your address is POSTed to your webhook_url.
POST /your/endpoint HTTP/1.1
content-type: application/json
x-mailmint-event: message.parsed
x-mailmint-delivery: dlv_01JQ8Z3K4M5N6P7Q8R9S
x-mailmint-signature: t=1787648043,v1=6a1f…c07b
{
"id": "msg_01JQ8Z3K4M5N6P7Q8R9S",
"mailbox": { "address": "k7m2xq4h9bwz@smooth-operator.online", … },
"envelope": { "from": "billing@acme.example", "tls": true, … },
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"fields": { /* value, confidence, source, evidence */ },
"flags": [], "needs_review": false, "status": "parsed"
}
# v1 = hex hmac_sha256(secret, t + "." + rawBody)
# Retries at 0s, 30s, 2m, 10m, 1h, 6h. 10s timeout each.Integrations
The schema editor lives inside n8n
Other email parsers make you leave your automation tool to build a template. The
n8n-nodes-mailmint community node does it on the canvas: name a field, give it a type and a
sentence, hit Execute step, see the value.
- No MailMint address needed. Put the node after n8n’s Email Trigger (IMAP) or a Gmail node and it parses what arrives.
- A second output for anything doubtful. Route Messages Needing Review Separately adds a Needs Review branch. No IF node.
- A trigger that registers its own webhook and verifies every HMAC before the workflow runs; polling mode for an n8n the internet cannot reach.
-
Eleven actions, zero runtime dependencies, MIT. Marked
usableAsTool, so an n8n AI Agent can call it. Version 0.1.2 is in n8n’s verified registry since 1 September 2026, so it installs on n8n Cloud too.
_needs_review at the top level, everything else under _meta.Community node + trigger
Parse, five message and five mailbox operations, webhook or polling trigger.
HTTPSigned webhooks
HMAC-SHA256, timestamped, six retries, several endpoints per mailbox.
HTTPPolling feed
GET /v1/events with a cursor, for receivers the internet cannot reach.
Machine-readable spec
Every public operation, for code generators and API tools.
How it compares
Against the four products people actually use
Every cell comes from the vendor’s own pricing page, help centre or published API schema, fetched on 25 August 2026. Where a vendor publishes no figure the cell is empty rather than guessed.
| Feature | MailMint | Mailparser | Parseur | Docparser | Zapier Parser |
|---|---|---|---|---|---|
| Per-field confidence | Yes — computed, with the evidence span | None | None — outcome is PARSEDOK / PARSEDKO | None, bar one arithmetic preset | None |
| Where a value came from | source + verbatim evidence | — | — | — | — |
| Variable-row line items | Typed array, with row_count and truncated | One parsing rule per column, then “explode”; output is a flat cross-product | Table Field — the best of the four; AI caps at 25 pages | Smart Tables: page 1 only, columns frozen at design time | Cannot. Staff answer is {{shotnumberOne}}… |
| Reads the email body | Yes | Yes | Yes | No — stated in their own help centre | Yes |
| Reads PDF attachment contents | Not yet — extractor built, not yet wired in | No — points you at Docparser | Yes | Yes | No — attachments arrive zipped together |
| Re-parse mail already received | Any stored message, any schema, from the original bytes | Last 300 only; original bytes not kept | Yes, within the retention window | Yes, bulk API | No — “there is no way to replay them” |
| Change the schema over the API | PATCH /v1/mailboxes/:id | — | No — templates are not programmable | Yes | — |
| Entry price | Free for 300/mo, then $9 for 5,000 — $0.0018 each | $29.95/mo for 250 emails — $0.1198 each, their own figure | €49/mo for 100 emails — €0.49 each | $39/mo for 100 documents — $0.39 each | Free |
| n8n node | Yes — 11 actions + a trigger, on npm | None | Official, published 21 Aug 2026 — upload + webhook only | None | Native to Zapier |
Scroll the table sideways to see every column.
Sources: mailparser.io/pricing and 183 Mailparser help-centre articles; docparser.com/pricing, docparser.com/api and 180 Docparser help-centre articles; parseur.com’s own pricing endpoint and its published OpenAPI 3.1 schema; Zapier’s community forum, including staff replies. Parseur’s figures came back in EUR because their pricing endpoint geolocates. These are good products. If your mail is single-sender plain text with no attachments and you already pay for an LLM, n8n’s own Information Extractor node is genuinely sufficient and free.
MailMint vs Mailparser
Full pricing and add-on tables, and when to stay where you are.
MailMint vs Parseur
Programmable schemas, per-field confidence, and the price per email.
MailMint vs Docparser
When the data is in the message body, not the PDF.
MailMint vs Zapier Email Parser
When 15 blended templates stop being enough.
What an email parsing API must get right
The category, from MIME to the review queue.
Worked examples
Invoices, order confirmations, shipping notices and leads, parsed live.
Pricing
Per parsed email, not per seat
A parsed email is one message MailMint accepted and returned JSON for. A message that arrives flagged still counts; a message that never reached us does not. Free needs no card, and every plan can be cancelled from the dashboard in two clicks.
Free
$0 /month
300 parsed emails a month
- Inbound address and
POST /v1/parse - Webhooks, polling and the n8n node
- Raw message kept 30 days
- Attachment bytes kept 7 days
- No card
- Over the line: still delivered, flagged
Enough to run a real low-volume workflow rather than only to look at one.
Create an accountStarter
$9 /month
5,000 parsed emails a month
- Everything in Free
- Raw message kept 90 days
- Attachment bytes kept 30 days
- Re-parse anything still inside the window
Mailparser’s entry plan is $29.95 for 250 emails. This is twenty times the volume for less than a third of the price.
Start on StarterPro
$29 /month
25,000 parsed emails a month
- Everything in Starter
- Raw message kept 180 days
- Attachment bytes kept 60 days
When several senders feed several mailboxes and a layout change has to be replayable months later.
Start on ProScale
$99 /month
150,000 parsed emails a month
- Everything in Pro
- Raw message kept 365 days
- Attachment bytes kept 90 days
Production volume, and a retention window that outlives an audit.
Start on Scalequota_exceeded and without the model pass, so fields the rules could not read come back
null rather than guessed.Prices in US dollars, billed monthly by Stripe. VAT is added where it applies.
Questions
Before you wire anything up
Do I need an inbound address to try it?
No. POST /v1/parse takes raw MIME, or just a subject, text and
html, plus your schema — and stores nothing at all. It is the same parser, the same
response shape, and the same confidence numbers. The quickstart starts there.
Where does the confidence number come from?
Not from the model. It is computed from signals we can verify: whether the quoted evidence is really a verbatim substring of the message; whether the deterministic rules layer and the model independently agreed; whether the invoice arithmetic reconciles; and whether the value coerced cleanly to its declared type. The model’s own self-report is one input with the smallest weight, and it may only lower a score. The full rule is in the docs.
What happens to a message that fails to parse?
It is still delivered. A message with any flag on it arrives at your webhook with
needs_review: true and the flags naming what went wrong and in which field. Nothing is ever
silently dropped, and a field that could not be found is null — never invented, never a
placeholder string.
Can I verify that a webhook really came from MailMint?
Yes. Every delivery carries x-mailmint-signature: t=<unix>,v1=<hex>, where the hex
is hmac_sha256(secret, t + "." + rawBody). The timestamp is inside the signed string, so a
captured request cannot be replayed. There are worked verification examples in
Node and Python — copy one, do not guess.
Is any of this live?
Yes. The API is live at mailmint.app.mintapis.com; the hosted inbound address works —
mail sent to your @smooth-operator.online address is received, authenticated and parsed; the
n8n node is on npm and in n8n’s verified registry; and paid plans are live through Stripe. What does
not exist yet: a published uptime history, an SLA, a SOC 2 report, or a customer logo or
testimonial we have permission to quote. See the status page.
What about prompt injection from a hostile email?
It is a real risk in any product that pipes attacker-controlled email into a model, and we will not
pretend it is solved. What exists is a structural mitigation, not a control: a value is only accepted with
a high confidence when its evidence is a verbatim substring of the message, when the deterministic layer
independently agrees, or when the arithmetic reconciles — none of which an instruction embedded in a
body can manufacture. Treat parsed output as data, and check needs_review before acting on
it.
Send one email and see what comes back
The shortest true path from nothing to structured JSON is one curl against
/v1/parse. No address, no DNS, nothing stored.
Who runs this. MailMint is built and run by Florian Standhartinger in Passau, Germany. It is new, so there are no customer logos, testimonials or uptime history on this page, and there will not be invented ones. Every number here is either measured and dated, or taken from a competitor’s own page and attributed. Not true yet: attachment contents are not extracted, and there is no SLA or SOC 2 report.