Every field carries a confidence and the text it was read from

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" } ] } }
  ]
}'
HTTP 200 model: DeepSeek-V4-Flash total: 4735 ms needs_review: false
4,735 ms
End to end on a real invoice email: 22 ms MIME, 14 ms deterministic rules, 4,611 ms model.
12 / 12  ·  3 / 3
Scalar fields correct, and line-item arrays complete, across three real invoices — USD, EUR with German descriptions, and GBP.
0
Competitors that return a per-field confidence, an evidence span, or any provenance — checked across 363 vendor help articles and two published API schemas.

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 raises rule_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_review is 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.

83.5 % / 91.5 %
Precision and recall on a hold-out set the parser had never seen — 36 adversarial messages, 163 labelled field slots. This is the number we publish.
91.6 %
Of field slots the deterministic layer answers with no model call at all, at a mean of 12 ms. Those values are byte-identical run to run.
92 · 83 · 57 · 20
Percent actually correct in the 0.9+, 0.7–0.9, 0.6–0.7 and sub-0.6 confidence buckets on that same hold-out. The curve declines. That is what makes the number worth reading.

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/.

fields GET /v1/messages/msg_01JQ8Z…
invoice_number "INV-2291" rule+llm
0.97

evidence Invoice INV-2291 from Acme Ltd

total 31.5 rule
0.97

evidence Total: $31.50  ·  line items sum to the total

po_number null none
0.00

Not in the message. Returned as null — never invented, never “N/A”.

vendor_vat_id "GB 123 4567 89" llm
0.30

The model quoted evidence that is not in the message. Score capped, field flagged.

flags missing_required:po_number hallucinated_evidence:vendor_vat_id needs_review: true

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.

docparser.com, verbatim

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…”

mailparser.io, verbatim

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.

zapier email parser

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.

  • inline and content_id are 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/:id serves the file, and ?include=attachments inlines 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_count and truncated ride on every table, and a table cut off at the cap raises table_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 flagged array_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 pairedItem set so n8n can still trace all forty items back to the one email.
1 · 40 · 520
Row counts that come out complete, deterministically, with no model involved and so no token limit to fall off.
61
<table> tags in one real Stripe invoice — and its line items are in none of them. That is why everyone else returns one row instead of forty.
flagged
On the adversarial hold-out set, every case whose rows came out wrong raised 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/raw hands the .eml back to you too.
  • Any stored message, any schema. POST /v1/messages/:id/reparse takes 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_version by 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.

69 / 69

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.

dkim: "body_altered"

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.

spf: "none"

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.

charset & encoding

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.

body.stripped_text

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.

envelope vs headers

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 .eml when 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_url to 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.

The MailMint node in n8n, showing the Fields editor with three fields: total (Number) with description "grand total including tax" and hint "labelled Total", and due_date (Date) with description "when payment is due" and hint "labelled Due".
Defining the schema on the canvas. Each field has a Name, a Type, a Description and a Hint — the Description is the single biggest lever on accuracy.
The same node after Execute step, with an output table showing columns invoice_number INV-2292, total 132, due_date 2026-09-15, _needs_review false, and a _meta column.
One item out, with _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 YesYesYes No — stated in their own help centreYes
Reads PDF attachment contents Not yet — extractor built, not yet wired in No — points you at Docparser YesYes 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.

Who runs this, and what it is not

MailMint is built and run by Florian Standhartinger. It is new, so there are no customer logos, no testimonials and no uptime history on this page — and there will not be any invented ones. Every number here is either measured on real email and named as such, or fetched from a competitor’s own page and attributed.

What is not true yet, in one list. No domain has been bought, so the hosted inbound address does not exist; the paths that work today are POST /v1/parse, POST /v1/test/deliver, and putting the n8n node after a mailbox you already own. n8n-nodes-mailmint is not published to npm. Attachment content is not extracted into attachments[].extracted yet, though the extractor exists. There is no pricing, no SLA and no SOC 2 report.

The measurement method behind the numbers in the hero is in docs/MODEL-BENCH.md, the competitor research with every source URL is in docs/COMPETITORS.md, and the frozen output contract is in docs/CONTRACT.md.