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:
| Field | Value | Conf. | Source | Verdict |
|---|---|---|---|---|
email | a.kowalczyk@helixdiag.pl | 0.97 | rule+llm | right |
phone | +48 22 501 88 40 | 0.97 | rule+llm | right |
country | Poland | 0.97 | rule+llm | right |
timeline | Q4 2026 | 0.97 | rule+llm | right |
contact_name | Dr. Anna Kowalczyk | 0.85 | llm | right |
company | Helix Diagnostics Sp. z o.o | 0.50 | rule | right, minus the final full stop |
message | the enquiry text | 0.50 | rule | right; rule and model disagreed on where it ends |
budget_max | 25000 | 0.25 | llm | right, 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.
The other three #
Comparisons instead: what an email parsing API has to get right · vs Mailparser · vs Parseur · vs Docparser · vs Zapier Parser