Skip to content

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