MailFrame’s shipped path is one synchronous endpoint. You POST /v1/parse with the raw email in
raw_mime and a JSON Schema, and typed, validated JSON comes back in the same HTTP response.
The quality of that extraction depends heavily on what you put in raw_mime. A frequent source
of poor results is not a schema problem at all. It is a transport problem: the caller strips the
message down to a body fragment before sending it, and the parser never sees the headers, the
multipart structure, or the plain-text alternative that would have made the extraction much
easier.
This post explains why the full raw message matters, exactly what raw_mime expects, how to
capture a multipart message intact from common sources, and the handful of mistakes that quietly
degrade results.
Why the raw message matters
A real email is more than its visible body. An RFC 822 message carries several layers of signal that the extraction pipeline uses:
- Headers.
From,To,Date,Subject, andMessage-IDidentify the sender, anchor the message in time, and give you a stable key for deduplication. A schema field likeorder_dateis easier to fill when theDateheader is part of the input, because the extractor has a second place to find the date if the body is ambiguous. - Multipart structure. Most transactional email is
multipart/alternativewith atext/plainpart and atext/htmlpart. The plain-text part is usually the cleanest source for extraction. If you send only the HTML, the parser has to work through markup and tracking pixels to find the same facts. - Charsets and transfer encodings. Each part declares its own
charsetandContent-Transfer-Encoding(oftenquoted-printableorbase64). MailFrame decodes these per part. If you decode them yourself and re-serialize incorrectly, currency symbols and accented names can arrive corrupted. - Attachments. Encoded attachment parts can only be told apart from the readable body when the multipart boundaries are intact. Break the boundaries and base64 attachment bytes can end up mixed into the text handed to extraction.
The pipeline design post covers how these stages fit together. The short version is that MIME extraction runs before the model ever sees the text, and it does its best work on the original message.
What raw_mime actually is
raw_mime is a JSON string containing the full RFC 822 message text. Headers, a blank line,
then the body, including every multipart boundary. That is the whole contract.
Three things it is not:
- It is not base64. You do not encode the message before putting it in the JSON string. The API hands the string directly to the MIME extractor.
- It is not a pre-extracted body. You do not need to pick the text part, strip HTML, or decode quoted-printable. Send the message as the mail server delivered it.
- It is not a file upload. There is no multipart form field for email. The message travels as a string inside a JSON request body.
Because it is a JSON string, the only transformation you apply is JSON escaping. Line breaks
become \r\n or \n inside the string, and any double quotes inside header values or boundaries
become \". Every JSON library does this for you when you serialize an object. The only time you
would hand-write those escapes is in a curl example like the one below.
A complete multipart example
Here is a small but structurally complete multipart/alternative receipt. It has real headers, a
declared boundary, a text/plain part, and a text/html part:
From: Acme Coffee <receipts@acmecoffee.example>
To: jordan@example.com
Date: Wed, 28 Oct 2026 09:14:02 +0000
Subject: Your receipt from Acme Coffee
Message-ID: <rcpt-7731@acmecoffee.example>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="b1_7731"
--b1_7731
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: 7bit
Thanks for your order on October 28, 2026.
Order #7731
Total charged: $29.99
--b1_7731
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: 7bit
<html><body><p>Thanks for your order on October 28, 2026.</p>
<p>Order <b>#7731</b></p><p>Total charged: <b>$29.99</b></p></body></html>
--b1_7731--
To send it, place that exact text in raw_mime with JSON escaping applied. The schema travels
inline in the same request. The newlines are written as \r\n, and the quotes around the
boundary and charset become \":
curl -X POST https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer $MAILFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema": {
"type": "object",
"required": ["merchant", "order_number", "total_amount", "order_date"],
"properties": {
"merchant": { "type": "string" },
"order_number": { "type": "string" },
"total_amount": { "type": "number", "minimum": 0 },
"order_date": { "type": "string", "format": "date" }
}
},
"raw_mime": "From: Acme Coffee <receipts@acmecoffee.example>\r\nTo: jordan@example.com\r\nDate: Wed, 28 Oct 2026 09:14:02 +0000\r\nSubject: Your receipt from Acme Coffee\r\nMessage-ID: <rcpt-7731@acmecoffee.example>\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative; boundary=\"b1_7731\"\r\n\r\n--b1_7731\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\nContent-Transfer-Encoding: 7bit\r\n\r\nThanks for your order on October 28, 2026.\r\nOrder #7731\r\nTotal charged: $29.99\r\n\r\n--b1_7731\r\nContent-Type: text/html; charset=\"UTF-8\"\r\nContent-Transfer-Encoding: 7bit\r\n\r\n<html><body><p>Thanks for your order on October 28, 2026.</p>\r\n<p>Order <b>#7731</b></p><p>Total charged: <b>$29.99</b></p></body></html>\r\n\r\n--b1_7731--\r\n"
}'
A clean parse returns HTTP 200 with the parse id, a status of completed, and the extracted
object under data. The values below are illustrative; the id is generated per request and the
fields depend on the model’s run against your input:
{
"id": "parse_9d41e2",
"status": "completed",
"data": {
"merchant": "Acme Coffee",
"order_number": "7731",
"total_amount": 29.99,
"order_date": "2026-10-28"
}
}
Nothing about the request changes when the message grows. A ten-part newsletter with inline images and a PDF attachment goes in the same field, as the same kind of string. Extraction targets the readable text parts of the message; attachment contents are not parsed as input today. See the JSON Schema email parser guide for more on designing the schema side of the request.
How to capture the raw message from common sources
The right way to get a message into raw_mime is to grab the original bytes and never reshape
them. The specific API differs by source, so check your provider’s documentation for the exact
call. The pattern is the same everywhere:
- IMAP. Fetch the whole message, not a body section. In most IMAP libraries this is the
RFC822orBODY[]item rather thanBODY[TEXT]or a specific part number. The result is the complete message with headers and boundaries. - Inbound email webhooks. Many inbound-email services can hand you the full raw message
alongside their pre-parsed fields, sometimes as a separate option or a separate field. Prefer
the raw form. The pre-parsed
textandhtmlfields are convenient for display but drop the structure MailFrame uses. - Object storage. If your mail pipeline writes messages to S3-style storage, the object is
typically the raw message already. Read it as bytes and convert to a string without loss.
Messages with 7-bit, quoted-printable, or base64 parts are plain ASCII on the wire, so this
is straightforward. Legacy messages with
8bitorbinaryparts in a non-UTF-8 charset need care: a blind UTF-8 decode of the whole message can replace bytes with U+FFFD and corrupt those parts before MailFrame sees them. Do not try to fix this by decoding the whole byte string with one charset, because different parts can declare different charsets. Instead, load the message with a MIME-aware library and re-serialize it with a 7-bit-safe transfer encoding such as quoted-printable or base64, which preserves each part’s bytes and charset declaration while producing an ASCII string you can place in JSON safely. - Mailbox exports.
.emlfiles are raw RFC 822 messages and can be read directly..mboxfiles are concatenated messages separated byFromenvelope lines. Do not split on those lines yourself: body lines that begin withFromare escaped in some mbox variants and not in others, and a naive line split can cut messages apart or merge them. Use a proper mbox parser to iterate the messages, keep each message’s full headers and body intact, and send each one individually.
When in doubt, look at the first line of what you are about to send. If it is a header like
Received:, From:, or Return-Path:, you have the raw message. If it is <html> or a
sentence of body text, you have a fragment.
Common mistakes
Each of these produces a request that succeeds but extracts worse than it should:
- Sending only the HTML part. The request may still return a result, but you have thrown away the plain-text alternative and every header. Dates, sender identity, and IDs that lived in headers are now missing, and the model is working through markup to find the rest.
- Base64-encoding the message.
raw_mimeis a plain string. If you base64-encode the whole message, the parser receives one long opaque token with no headers and no body to extract from. - Dropping headers to “save space”. Headers are small and carry some of the most reliable fields in the message. Keep them.
- Mangling boundaries. If a templating step, a trim, or an overzealous whitespace
normalizer alters the
--boundarylines, the parser can no longer split the parts, and the HTML, plain text, and attachment bytes may merge into one blob. Do not touch the message content between fetching it and serializing it into the JSON string. - Decoding parts yourself. Converting quoted-printable or per-part base64 before sending is
unnecessary and error-prone. The extractor handles this in two distinct steps for each part:
it first reverses the part’s
Content-Transfer-Encodingto recover the original bytes, then decodes those bytes to text using the charset declared on that part. - Reaching for
fileto send an email. Thefilefield is not for email. See the next section.
Base64 is transport encoding, not the email format
The parse request has two input fields, and they have different encodings for a reason.
raw_mime is a plain JSON string. The server passes it straight into the MIME extractor, which
understands the message’s own internal encodings. Base64 appears inside a MIME message only as a
Content-Transfer-Encoding on individual parts, and the extractor decodes those parts itself.
You never add an outer layer.
file is different. It is designed for binary documents such as PDFs and images, which cannot
be placed in a JSON string as-is. For that field, base64 is the transport encoding: the server
decodes the base64 string back to bytes before processing. PDF and image input through file is
on the roadmap, not yet available, so today every request should use raw_mime.
The simple rule: if the input is an email, it is text, and it goes in raw_mime without any
encoding. Base64 is only for binary input, and binary input is a planned feature.
Roadmap context
The only shipped capability described here is the direct, synchronous POST /v1/parse call with
raw_mime plus a schema, returning typed JSON in the HTTP response. The sync request has a 30
second timeout.
Two related inputs are planned, not yet available:
- Inbox forwarding to a provisioned address, so messages arrive at MailFrame without your code making the API call. When it ships, the message that reaches the parser will be the same raw MIME you would otherwise send yourself, so everything in this post still applies.
- PDF and image input through the base64
filefield, as described above.
If you are planning around delivery of results rather than ingestion, the webhook design post describes the planned approach. Webhook delivery is also on the roadmap and not yet available.
Production checklist
Before you point real traffic at the endpoint:
- Preserve the original message. Fetch the full message from your source and keep its
content unmodified until JSON serialization. Do not trim, re-wrap lines, or rewrite boundary
lines. Convert bytes to a string losslessly. For legacy
8bitorbinaryparts, load the message with a MIME-aware library and re-serialize it with a 7-bit-safe transfer encoding; do not decode the entire message with one charset. - Let your JSON library escape it. Build an object with
schemaorschema_idandraw_mime, then serialize. Hand-escaping is only for curl demos. - Pick
schema_idor an inlineschema. A savedschema_idkeeps requests small and lets you evolve the contract in one place. An inlineschemais fastest for iteration. Either way the request must include exactly one of them. The schema-first rationale explains why the contract lives on the request at all. - Check three things before storing. Confirm the HTTP response is OK, that
statusiscompleted, and thatvalidation_errorsis absent or empty. Only then treatdataas schema-conformant. A response that carries a non-emptyvalidation_errorsarray means the extraction did not fully satisfy the schema and deserves a review path. Passing validation proves the shape is right, not that every value is semantically correct, so keep a spot-check or reconciliation step for high-stakes fields. A non-2xx response carries the standarderrorenvelope withtype,message, andrequest_id, and no parse data. - Keep
Message-ID. Since you are sending the headers anyway, store theMessage-IDnext to the parse result so repeated deliveries of the same message do not create duplicate records.
Next steps
- The API reference documents every field on the parse request, including
schema_idand the full error envelope. - The email-to-JSON use case overview shows where a raw-MIME parse fits in a larger application.
- Browse the Stripe receipts schema for a pre-built example, or try the free Stripe receipt parser in the browser with no API key.
- Ready to send real messages? Apply for early access.