An email address in.
Structured JSON out.
MailMint gives you an inbound address. Mail sent to it comes back as the fields you asked for — each one with a confidence we computed, the layer it came from, and the verbatim substring of the message the value was read out of. Delivered by webhook, by polling, or into n8n.
You do not need an inbound address to start. POST /v1/parse takes
raw MIME — or just a subject and a body — and stores nothing.
# 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\nItem Qty Amount\nWidget 3 $27.00\nShipping 1 $4.50\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" },
{ "name": "line_items", "type": "array", "items": {
"type": "object", "fields": [
{ "name": "description", "type": "string" },
{ "name": "qty", "type": "integer" },
{ "name": "amount", "type": "number" } ] } }
]
}'{
"id": null, // /v1/parse stores nothing, so there is no message id
"mailbox": null,
"received_at": "2026-08-25T09:14:03.221Z",
"headers": { "subject": "Invoice INV-2291 from Acme Ltd", "date": null, /* … */ },
"body": { "text": "Invoice INV-2291\n\nItem Qty…", "html": null,
"stripped_text": "…", "language": "en" },
"attachments": [],
"auth": { "spf": null, "dkim": null, "dmarc": null, "spam_score": null },
"tables": [
{ "source": "text", "index": 0,
"headers": ["Item", "Qty", "Amount"],
"rows": [["Widget", "3", "$27.00"], ["Shipping", "1", "$4.50"]],
"records": [{ "Item": "Widget", "Qty": "3", "Amount": "$27.00" }, /* … */],
"row_count": 2, "truncated": false }
],
"detected": { "type": "invoice",
"amounts": [{ "value": 31.5, "currency": "USD", "raw": "$31.50" }, /* … */],
"dates": [{ "value": "2026-09-08", "raw": "Sep 8, 2026" }],
"ids": [{ "kind": "invoice_number", "value": "INV-2291" }],
"emails": [], "urls": [], "phones": [], "addresses": [] },
"fields": {
"invoice_number": { "value": "INV-2291", "confidence": 0.97,
"source": "rule+llm", "evidence": "Invoice INV-2291" },
"total": { "value": 31.5, "confidence": 0.97,
"source": "rule", "evidence": "Total: $31.50" },
"due_date": { "value": "2026-09-08", "confidence": 0.97,
"source": "rule", "evidence": "Due: Sep 8, 2026" },
"line_items": { "value": [{ "description": "Widget", "qty": 3, "amount": 27 },
{ "description": "Shipping", "qty": 1, "amount": 4.5 }],
"confidence": 0.97, "source": "rule+llm", "evidence": null }
},
"flags": [],
"needs_review": false,
"parse": {
"request_id": "req_9f2c…",
"schema_version": null,
"model": "deepseek-ai/DeepSeek-V4-Flash-0731-TEE",
"llm_used": true,
"timings_ms": { "total": 4735, "mime": 22, "deterministic": 14,
"llm": 4611, "persist": 0 },
"warnings": []
},
"raw_url": null
}# Mail sent to your address is POSTed to your webhook_url.
# The body is exactly the object in the "response" tab, with
# id, mailbox and raw_url filled in.
POST /your/endpoint HTTP/1.1
content-type: application/json
x-mailmint-event: message.parsed
x-mailmint-delivery: dlv_01JQ8Z3K4M5N6P7Q8R9S
x-mailmint-timestamp: 1787648043
x-mailmint-signature: t=1787648043,v1=6a1f…c07b
{
"id": "msg_01JQ8Z3K4M5N6P7Q8R9S",
"mailbox": { "id": "mbx_01JQ8Y…",
"address": "k7m2xq4h9bwz@parse.example.com",
"name": "Invoices" },
"envelope": { "from": "billing@acme.example",
"to": ["k7m2xq4h9bwz@parse.example.com"],
"remote_ip": "203.0.113.7", "tls": true },
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam_score": 0.4 },
"fields": { /* … as above … */ },
"flags": [], "needs_review": false, "status": "parsed",
"raw_url": "https://…/v1/messages/msg_01JQ8Z…/raw"
}
# Signature: t=<unix>,v1=hex hmac_sha256(secret, t + "." + rawBody)
# Retries at 0s, 30s, 2m, 10m, 1h, 6h. Six attempts, 10s timeout.Measured on 25 August 2026 on real email — three Stripe
invoices that travelled the internet and arrived with valid DKIM, not fixtures. The model that
answered was deepseek-ai/DeepSeek-V4-Flash-0731-TEE. Three messages from one sender is a
small sample. It is real mail, which is why it is the number we quote, and it is not a broad accuracy
claim — the full method is in docs/MODEL-BENCH.md in the repository.
The thing people actually ask for
“How do I find out that it went wrong?”
Across the n8n, Make, Zapier and Reddit threads we read, almost nobody asks for better accuracy in the abstract. They ask that one question. It has no answer in this category, because not one competitor returns a per-field confidence, an evidence span, or a “where did this come from” pointer.
“You can have a 0 % error rate in n8n and a 15 % bad-data rate downstream. That’s the one that corrupts your database quietly for weeks before anyone notices.”
“A model can match the JSON schema perfectly while quietly inventing one row.”
— two practitioners, r/n8n
Every value arrives with its receipt
Four keys per field, always: value, confidence,
source and evidence — where the evidence is a verbatim substring of
the message the value was read out of, so you can point at it.
- Confidence is computed, never taken from the model. A practitioner’s warning we took seriously: “a model will report 0.95 on a PO code it hallucinated.” So the number is built from signals we can verify.
-
The evidence check actually runs. If the claimed span is not really in the message,
the score is cut and capped and the field is flagged
hallucinated_evidence:<field>. That is a deterministic test, not an opinion. -
Two extractors, independently. A deterministic rules layer and the model both run.
Agreement earns
"source": "rule+llm"and a high score. Disagreement keeps the rule’s value, drops the confidence and raisesrule_llm_disagreement:<field>. Nothing else in this market runs both, so nothing else can detect the disagreement. -
The arithmetic has to reconcile. Do the line items sum to the subtotal, and does
subtotal + tax + shipping − discount equal the total? Failing sets
arithmetic_mismatch. This is what catches “forty rows in the mail, one row in the output”. - A matched rule wins, so the answer is reproducible. For every field the deterministic layer resolved, the output is byte-identical run to run — the property the incumbents sell and an LLM pipeline usually gives up. The model runs anyway, to cross-examine rather than overrule. How that works.
-
One boolean to route on.
needs_reviewis true when any field is low-confidence, missing but required, uncoercible, or backed by evidence that is not in the message. Filter the API by it, or let the n8n node send those messages down a second output.
The model’s own self-reported number is one input, and it is only ever allowed to lower a score — never to raise one above what the verifiable signals justify.
Measured 25 August 2026. These are hold-out numbers, deliberately.
On our own labelled corpus the full pipeline scores 100 % precision and recall, and we do not
quote that, because a number measured on the cases you tuned against is not a number. 36 unseen
adversarial messages is still a small sample and not a general accuracy guarantee. The harness, the
corpus and every case it gets wrong are in packages/parser/.
evidence Invoice INV-2291 from Acme Ltd
evidence Total: $31.50 · line items sum to the total
Not in the message. Returned as null — never invented, never
“N/A”.
The model quoted evidence that is not in the message. Score capped, field flagged.
Illustrative values in the real response shape. What competitors do instead: Mailparser’s documented handling for a rule that returns nothing is a “Set Default Value” filter that substitutes a value you chose — which destroys the signal. Zapier’s own community documents the worse failure: “instead of leaving the cell empty, it starts pulling data from another random bit of the email”, with Zapier’s Community Manager confirming there is no way to mark a template field optional. And every vendor alert in the category is transport-level — it fires when your endpoint returns a 5xx, never when the parse was wrong.
The seam in the market leader
The message and its attachments, in one object
The most common real workflow in the whole forum corpus — an invoice arrives as a PDF attached to an email, give me the fields — is the one workflow the incumbents split down the middle.
Cannot read an email body
“Docparser is not capable of extracting data stored in the body text or subject of an email… If you are looking for a great email parser solution, our sister app Mailparser.io is an industry-recognized leader…”
Cannot read a PDF attachment
On extracting table cells from PDF attachments: “please check out Docparser and see which is the best fit for your PDF conversion needs”. Same company. Two products. Two subscriptions. The pointer goes in a circle.
Zips them together
All attachments arrive as one blob, so you cannot address a specific file — a standing,
acknowledged, unfixed feature request. In one documented case an Outlook signature logo,
image001.png, was fed to an LLM instead of the customer’s invoice.
What MailMint returns today
Every attachment is its own addressable object in the same response as the body. Nothing is zipped, nothing is flattened into the same namespace as our own metadata.
-
inlineandcontent_idare on every attachment. That pair is exactly what separates the invoice from the signature logo, and it is the failure that broke the Zapier case above. -
The bytes, not just a link.
GET /v1/attachments/:idserves the file, and?include=attachmentsinlines it as base64 in the message. Mailparser hands you a URL that expires with retention and never the bytes. - 25 MB inbound, 10 MB per stored attachment. A message over the cap is still received and still parsed; what changes is what we keep, and you are told which with a flag, never by silence.
Built, not yet wired up — said plainly
MailMint does not read text out of a PDF attachment yet. The extractor itself exists
— packages/docs does PDF text and tables, spreadsheets, DOCX and an OCR
fallback — the response shape has the slot for it
(attachments[].extracted), and the n8n node already knows how to fan line items out
of it. What does not exist yet is the wire between them, so 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. This paragraph describes the code as of
25 August 2026 and changes when the wiring does.
"attachments": [
{
"id": "att_01JQ8Z3K4M5N6P7Q8R9S",
"filename": "invoice-2291.pdf",
"content_type": "application/pdf",
"size": 48213,
"sha256": "9f2c8b…",
"inline": false,
"content_id": null,
"url": "https://…/v1/attachments/att_01JQ8Z…"
},
{
"filename": "image001.png",
"content_type": "image/png",
"size": 4102,
"inline": true, // <- the signature logo
"content_id": "<image001@acme>"
}
]
// Any number of rows. One array. row_count and truncated
// are published so a short array can never be silent.
"tables": [{
"source": "html", "index": 0,
"headers": ["Item", "Qty", "Unit", "Amount"],
"records": [
{ "Item": "Widget", "Qty": "3", "Unit": "$9.00", "Amount": "$27.00" },
/* … 38 more … */
],
"row_count": 39,
"truncated": false
}]
// Zapier's official answer to the same problem:
// {{shotnumberOne}} … {{shotnumberN}}, hand-written,
// against a hard cap of 15 templates.
The #1 complaint in this market
Forty rows in the mail, forty rows in the output
“I got one row instead of forty” is the single most common unanswered question across the n8n, Make and Zapier forums — roughly fourteen threads, and the one where the honest answer from every incumbent is that no answer exists.
-
A real typed array, not a cross-product. Declare a field as
"type": "array"with"items": {"type":"object"}and you get a nested array. Mailparser’s equivalent is one parsing rule per column, then an “explode” step, and its output repeats the order number on every row. -
A short table is loud, not silent.
row_countandtruncatedride on every table, and a table cut off at the cap raisestable_truncated. -
More than one extractor, then reconciled. Real HTML invoices frequently do not
contain a real
<table>grid at all — one of the real Stripe invoices we measured on has 61<table>tags and its line items are in none of them, because Outlook compatibility pushes every large sender towards nested single-cell tables. That is why everyone else returns one row instead of forty. So rows are found three ways — the HTML grid, repeating DOM blocks, and aligned text — and the candidates are reconciled. When two sources disagree the field is flaggedarray_source_disagreement, not quietly resolved. -
In n8n, one row becomes one item. Set Output to One Item Per Line
Item and the header fields repeat on each row, with
pairedItemset so n8n can still trace all forty items back to the one email.
arithmetic_mismatch or array_source_disagreement. A wrong table does
not arrive silently.What is true and what is not, on line items. Complete row
counts are reliable. Per-row content is not yet — on a deliberately adversarial
hold-out set (sums that do not reconcile, decoy totals in footers, items split across a quoted
reply) the reconciler still gets rows wrong, and we would rather say so than average it away. What
it does not do is get them wrong quietly. Measured 25 August 2026; the harness is in
packages/parser/test/holdout/.
# 1. The sender changed their layout a fortnight ago.
# Fix the schema over the API.
curl -X PATCH $MAILMINT_URL/v1/mailboxes/mbx_01JQ8Y… \
-H "Authorization: Bearer $MAILMINT_API_KEY" \
-d '{"schema": [ … ]}'
# 2. Dry-run it over everything you already received.
# Writes nothing. Shows you what would change.
curl -X POST $MAILMINT_URL/v1/mailboxes/mbx_01JQ8Y…/reparse \
-d '{"dry_run": true, "since": "2026-08-01"}'
# 3. Run it for real. Re-delivery is a SEPARATE switch,
# so tuning cannot fire a month of webhooks at you.
curl -X POST $MAILMINT_URL/v1/mailboxes/mbx_01JQ8Y…/reparse \
-d '{"redeliver": false}'
# Every result says which schema produced it:
"parse": { "schema_version": 4, … }
The hardest “no” in the category
Re-parse mail you already received
Zapier staff, verbatim: “there is no way to replay them.” Mailparser re-parses only the last 300 emails and does not keep the original bytes at all — its own knowledge base calls them “of ephemeral nature to us”.
-
We keep the original RFC822 bytes and re-run against them, so a re-parse is the real
message and not a re-reading of our own earlier output.
GET /v1/messages/:id/rawhands the.emlback to you too. -
Any stored message, any schema.
POST /v1/messages/:id/reparsetakes a one-off schema in the body, so you can try a change against yesterday’s real mail before saving it. - Re-parse and re-deliver are separate switches, both defaulting to off. A tuning session that quietly re-fires a month of webhooks is how you get duplicate rows in someone else’s database.
-
Schema changes go over the API. Parseur’s own developer docs state you cannot
create or update templates programmatically — so the loop change schema → re-parse
→ compare is not scriptable anywhere else in this market. Docparser sells
“Parser Version Control” as a $8.33–$9.95/month add-on; every MailMint result
carries
parse.schema_versionby default.
Everything either side of the model
The unglamorous half, done properly
n8n already ships an Information Extractor node. “We have an LLM” is not a pitch. The value is the part nobody wants to write: turning a real MIME message into clean text, and telling you honestly who sent it.
Real DKIM signatures verify
SPF, DKIM and DMARC are evaluated for real against live DNS — not read off somebody else’s
header. Measured against 69 genuine third-party signatures from senders including gmail.com,
Fastmail, Pobox and GMX; all 69 verify, and 33 of them use relaxed/simple
canonicalisation, which is the combination naive implementations get wrong.
A forward is not a forgery
A DKIM body hash breaks whenever a message is modified after signing — which is what forwarding,
mailing lists and link-rewriting security gateways all do. So that gets its own value, distinct
from fail. Write if (auth.dkim === "fail") and you catch forgeries, not
your colleague’s Gmail forward.
We say when we do not know
On the hosted Cloudflare intake path the receiving worker gets no client IP, so SPF genuinely
cannot be evaluated. That is reported as none — never as a pass we did
not compute. The same rule applies to every field on the page.
Mojibake is a defect, not weather
Quoted-printable decoded, legacy charsets normalised to UTF-8, RFC 2047 encoded words resolved, RFC 2231 filenames reassembled. Garbled non-UTF-8 output is an open, staff-acknowledged defect in n8n’s own IMAP node, and it is the universal first breakage in every home-made pipeline.
The reply chain and signature removed
You get the raw text, the HTML, text rendered from the HTML when there is no plain part, and a stripped body with the quoted thread and the signature taken off — which is usually the one you actually want to extract from.
Two different senders, kept apart
envelope.from is what the SMTP conversation said; headers.from is what
the message claims. They are frequently not the same, the difference is the whole basis of sender
trust, and both are returned rather than merged into one convenient lie.
n8n community node
The schema editor lives inside n8n
Every other email parser makes you leave your automation tool, open their web app, upload a sample, click on the parts you want, and then go back to n8n and receive whatever they decided to send. This node does the whole thing on the canvas: name a field, give it a type and a sentence of description, hit Execute step, see the value.
-
No MailMint address needed to start. Put a MailMint node after n8n’s own
Email Trigger (IMAP) or a Gmail node and it parses what arrives. Input:
Automatic takes the raw
.emlwhen the trigger is set to RAW and the subject/text/html fields when it is not — and it will not mistake an attached PDF for the message. - A second output for anything doubtful. Turn on Route Messages Needing Review Separately and the node grows a Needs Review branch. No IF node, no expression.
-
The trigger registers its own webhook. On activation it sets the mailbox’s
webhook_urlto the workflow URL and installs a signing secret; every delivery has its HMAC verified before the workflow runs. Polling mode is there for an n8n the internet cannot reach. -
Eleven actions, zero runtime dependencies, MIT. Parse, five message operations, five
mailbox operations — including Reparse every message in a mailbox — and the main node
is marked
usableAsTool, so an n8n AI Agent can call it directly.
Not published yet
n8n-nodes-mailmint is not on npm. The screenshots on this page are the node
running in a real n8n instance, installed from a local build. Until it is published you cannot
install it from Settings → Community Nodes, and n8n Cloud will not take it at all
until it also passes n8n’s verification.
_needs_review at the top level and everything else
tucked under _meta. That is Simplify, which is on by default.How it compares
Against the four products people actually use
Every cell below 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.
| 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 | Not decided yet — see below | $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 (not yet 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; a US visitor sees different absolute numbers. The “no per-field confidence” finding is an absence we checked rather than a claim we read: 363 vendor help articles and two published API schemas, grepped. 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, and we are not going to win you.
Pricing
Not decided yet
Placeholder — do not plan against these numbers
There is no pricing decision for MailMint, and no tiers to show you. Nothing on this page is an offer.
What is decided is the shape of it. Inbound mail on the production path costs us $0.00 per message — Cloudflare Email Routing is free at any volume we will plausibly reach — and the only variable cost is one model call, on a chain headed by a model measured at a fraction of a cent per invoice. Meanwhile Mailparser charges $0.1198 per email by its own arithmetic and does no AI at all, and Postmark delivers parsed inbound JSON with base64 attachments at $0.00165 per email. MIME handling is not a moat. Whatever we charge will be for the extraction layer and its trust signals, and it will not look like $29.95 for 250 emails.
When there is a number, it will appear here and nowhere before.
Questions
Before you wire anything up
Do I need an inbound address to try it?
No, and that matters, because the hosted address depends on a domain being configured.
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 still 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 you?
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?
Not yet, and we would rather say so than imply otherwise. No domain has been bought, so no hosted inbound address exists; the n8n node is not on npm; there is no uptime history, no customers and no SLA. What does exist is the code, the parser, the node, and the measurements quoted on this page, all of which you can reproduce from the repository.
What about prompt injection from a hostile email?
It is a real risk in any product that pipes attacker-controlled email bodies into a model, and we
will not pretend it is solved. What exists today is a partial, structural mitigation rather than 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.