Parsing lead and contact-form emails

An enquiry arrives as an email — from your own contact form, from a portal, from a directory, from a partner who forwards them by hand. Getting it into the CRM as a record rather than as an unread message is a five-minute job done four hundred times a year. This page does it, and is honest about the fields that came back doubtful.

Measured on 30 August 2026 against the live service at https://mailmint.app.mintapis.com, from a free account created the same morning. Eight fields were asked for; three of them ended up in the review queue, and this page shows all eight rather than the five that worked.

Lead mail into a CRM record #

A lead is only worth something while it is warm, and the thing that cools it is sitting in a shared mailbox waiting for somebody to retype it. What you want is: the person, the company, a way to reach them, whatever qualification the form collected, and the message itself — created as a record, routed to an owner, within seconds of arriving.

Why leads are the messiest of the four #

Invoices and despatch notes are written by machines to a template. Lead mail is half machine and half human, and the human half is where the trouble is:

  • Every source has its own shape. Your form, a portal's notification, a directory's lead alert and a colleague's forwarded mail carry the same facts in four layouts — and the forwarded one has the real sender buried in a quoted header.
  • The valuable fields are free text. "Budget: 15,000–25,000 EUR" is a range, not a number. "Timeline: before our audit in November" is a date only a human can see.
  • Wrong is expensive in both directions. A misfiled company name is a bad CRM record; a lead that never became a record at all is lost revenue. This is exactly the case where you want to know which fields to trust, not an average.

The schema #

[
  { "name": "contact_name", "type": "string", "description": "full name of the person enquiring" },
  { "name": "company",      "type": "string", "description": "company name" },
  { "name": "email",        "type": "string", "description": "reply-to email address of the enquirer" },
  { "name": "phone",        "type": "string", "description": "phone number in international format" },
  { "name": "country",      "type": "string", "description": "country of the enquirer" },
  { "name": "budget_max",   "type": "number", "description": "upper end of the stated budget, as a number" },
  { "name": "timeline",     "type": "string", "description": "when they want to start" },
  { "name": "message",      "type": "string", "description": "the free-text body of the enquiry" }
]

A real enquiry, really parsed #

A contact-form notification: a Polish company, a labelled block of form fields, a free-text message and a footer with a timestamp and an IP. Back in 2,205 ms. All eight fields, nothing omitted:

FieldValueConf.SourceVerdict
emaila.kowalczyk@helixdiag.pl0.97rule+llmright
phone+48 22 501 88 400.97rule+llmright
countryPoland0.97rule+llmright
timelineQ4 20260.97rule+llmright
contact_nameDr. Anna Kowalczyk0.85llmright
companyHelix Diagnostics Sp. z o.o0.50ruleright, minus the final full stop
messagethe enquiry text0.50ruleright; rule and model disagreed on where it ends
budget_max250000.25llmright, but no evidence span survived

Flags: rule_llm_disagreement:company, rule_llm_disagreement:message, hallucinated_evidence:budget_max, plus a low_confidence: flag on each of the three. needs_review: true.

All eight values are usable, and three were held back anyway. That is the point of the page. company differs between the deterministic rule and the model by one character — the trailing full stop of Sp. z o.o. — and a disagreement is a disagreement whether or not it is trivial, so the confidence is capped at 0.50 rather than resolved in favour of a guess. budget_max is the interesting one: the model correctly read 25,000 as the upper end of "15,000–25,000 EUR", but that number is a derived answer, not a substring, so no evidence span survived a literal search of the message and it dropped to 0.25.

A value that cannot point at the text it came from is exactly where a parser invents things. The rule costs you review on some correct answers — and it is the reason a wrong company name does not quietly become a CRM record.

Choosing a threshold #

Because the confidence is per field, you do not have to treat the whole message as one decision. A lead pipeline usually wants the opposite of an invoice pipeline: create the record anyway, flag the fields. A lead held in a queue for review is a lead going cold.

# create every lead; mark the thin fields for the owner to confirm
const lead = Object.fromEntries(
  Object.entries(msg.fields).map(([k, f]) => [k, f.value]));

lead.confirm = Object.entries(msg.fields)
  .filter(([, f]) => f.confidence < 0.8)
  .map(([k]) => k);            // ["company", "message", "budget_max"]

Contact details are the fields worth a hard gate: email and phone came back at 0.97 here because a rule and the model agreed, and if they ever do not, that is worth a human before anybody dials. The calibration set behind these numbers is small — 163 labelled field slots on 36 hold-out messages, run three times — so treat 0.8 as a starting point you tune against your own mail, not as a probability. What the measurement does support is the direction of the gate: on that set, values reported at 0.9+ were right 189 times out of 189 and values at 0.7–0.9 only 69.9 %.

Into the CRM #

Point webhook_url at the CRM's inbound endpoint, or at the automation platform in front of it, and the signed message object arrives as the enquiry does. Delivery retries with backoff, a webhook that keeps failing auto-disables rather than silently dropping, and one endpoint failing never disables another — so the CRM and the Slack notification are independent.

In n8n: n8n-nodes-mailmint installs on n8n Cloud and self-hosted n8n, and its trigger falls back to the polling feed when a webhook cannot reach you. If community nodes are disabled, use an HTTP Request node against POST /v1/parse.

And if you later add a field — nobody asked for source until the first quarterly report — POST /v1/mailboxes/{id}/reparse replays the stored originals against the new schema, with dry_run: true to see what would change first. The leads you already have get the new field too.

When not to use this #

  • You own the form. Post it to your CRM directly. Structured data beats parsing an email you generated yourself — and if the form is on your own site, there is no reason for the data to take a detour through a mailbox at all. This disqualifies more readers of this page than anything else on it.
  • Your CRM already has an inbound address. HubSpot, Pipedrive and Salesforce all parse common lead notifications themselves. Check before you add a layer.
  • The mail is unstructured prose. "Hi, saw your talk, can we chat?" has no fields to extract. A parser adds nothing to a reply.
  • Under a hundred leads a month. Reading them yourself is better qualification than any schema, and free.

What we cannot claim #

MailMint has paying customers, but no customer logo, testimonial or case study is quoted without permission. There is no published uptime history, SLA or SOC 2 report. One region, one instance. The confidence numbers rest on 163 labelled field slots over 36 hold-out messages, run three times: enough to show that the top of the scale carries information — 189 of 189 values reported at 0.9+ were right, while the preceding run and its five errors remain published — and nowhere near enough to fit a reliability curve. This is one enquiry, parsed once; a second run of the same message returned contact_name at a different confidence, so treat individual numbers as observations, not constants. PDF and document attachments are not read yet. Prompt injection is not solved: an email body is attacker-controlled text, and parsed output is data to be checked, not an instruction to be followed.

Try it on your own worst email #

Not a demo message — the one your current parser gets wrong. Paste its subject and body into POST /v1/parse and read the confidence and the evidence span on each field. /v1/parse stores nothing, needs no inbound address and no DNS record, and the free plan is 300 parsed emails a month with no card, which is enough to run a real low-volume workflow rather than only to look at one.

Get an API key   Read the quickstart   API reference

The other three #

Invoice emailsSupplier invoices into ledger rows, with the arithmetic checked.
Order confirmationsOrder number, totals and every line item, reconciled against the total.
Shipping notificationsTracking number, carrier and the promised date, out of any carrier's template.

Comparisons instead: what an email parsing API has to get right · vs Mailparser · vs Parseur · vs Docparser · vs Zapier Parser