MailFrame’s shipped path is a single endpoint. You POST /v1/parse with the raw email in raw_mime
and a JSON Schema in schema, and you get typed, schema-validated JSON back synchronously in the
same HTTP response.
The part that surprises people on their first call: raw_mime does not have to be a real RFC 822
message pulled from a mail server. A plain-text string that looks like an email is enough. That
means you can try the full extraction pipeline from a terminal in under a minute, with no real
inbox, no saved schema, and no base64 encoding.
This post walks through exactly that: the smallest request that exercises the real API end to end.
What counts as email-like input?
raw_mime accepts raw RFC 822 MIME text: headers, a blank line, then the body. Full messages with
multipart bodies and encoded attachments work. But the parser does not reject input just because it
is short or missing most headers. A single Subject: line, a blank line, and a few lines of body
text is a valid message:
Subject: Your receipt from Acme Coffee
Thanks for your order on October 26, 2026.
Total charged: $29.99
That is the whole input. MailFrame treats the headers as headers and the rest as the body, then hands the normalized text to the extraction step.
Define a tiny inline schema
The schema field takes a standard JSON Schema object. You describe the fields you want back, their
types, and any constraints. The extraction is validated against this contract before it is returned.
Here is a three-field schema for the receipt above:
{
"type": "object",
"required": ["merchant", "total_amount", "order_date"],
"properties": {
"merchant": { "type": "string" },
"total_amount": { "type": "number", "minimum": 0 },
"order_date": { "type": "string", "format": "date" }
}
}
Two details matter here. total_amount is typed as a number, so you get 29.99 back rather than
the string "$29.99". And order_date uses the date format, so the model must normalize
“October 26, 2026” to 2026-10-26 or the field fails validation.
Send the request
Put both pieces in one JSON body. The newlines in raw_mime are written as \n inside the JSON
string:
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", "total_amount", "order_date"],
"properties": {
"merchant": { "type": "string" },
"total_amount": { "type": "number", "minimum": 0 },
"order_date": { "type": "string", "format": "date" }
}
},
"raw_mime": "Subject: Your receipt from Acme Coffee\n\nThanks for your order on October 26, 2026.\nTotal charged: $29.99\n"
}'
No schema registration, no setup beyond an API key. The schema travels with the request.
Read the response
A clean parse returns HTTP 200 with the parse id, its status, and the extracted object under
data. The response below is illustrative: the shape is what the API returns, but the id and
the extracted values depend on the model’s run against your input:
{
"id": "parse_3c91ab",
"status": "completed",
"data": {
"merchant": "Acme Coffee",
"total_amount": 29.99,
"order_date": "2026-10-26"
}
}
A schema mismatch does not fail the request. When the extracted output violates your schema, the
API still returns HTTP 200 with status: "completed", the parse id, and the best-effort data,
plus a validation_errors array describing what did not match. The array is omitted from the body
when there is nothing to report, and when present each entry is an object with a field and a
message, not a bare string. Treat a missing array the same as an empty one, and check it before
you trust data.
HTTP 422 is reserved for a different case: the extraction step itself fails and cannot produce any
output to validate. That response carries no parse id or data. Instead it uses the same error
envelope as every other failure: a body whose single top-level error object holds type,
message, and request_id.
A bad API key, an exhausted quota, or malformed JSON use that envelope too, with their own status
codes. So the HTTP status is your first routing signal: a 2xx means you have a parse result to
inspect, including any validation_errors, and the error envelope is the only shape you will see
on a non-2xx response.
The helper below is self-contained and meant to run server-side in Node, where it reads your API
key from the MAILFRAME_API_KEY environment variable and fails fast if it is missing. It sends the
request, branches on the status code first, and returns a plain result object. What you do with each
outcome, such as inserting a row or queuing the parse for review, is up to your application:
interface ValidationError {
field: string;
message: string;
}
interface ParseResult {
id: string;
status: string;
data: Record<string, unknown>;
validation_errors?: ValidationError[];
}
interface ErrorEnvelope {
error: { type: string; message: string; request_id: string };
}
type ReceiptOutcome =
| { kind: "valid"; id: string; data: Record<string, unknown> }
| { kind: "needs_review"; id: string; data: Record<string, unknown>; validationErrors: ValidationError[] }
| { kind: "request_error"; httpStatus: number; type: string; message: string; requestId: string };
async function parseReceiptText(rawText: string, schema: object): Promise<ReceiptOutcome> {
const apiKey = process.env.MAILFRAME_API_KEY;
if (!apiKey) throw new Error("MAILFRAME_API_KEY is not set");
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ schema, raw_mime: rawText }),
});
if (!res.ok) {
// Every failure, including a 422 extraction failure, is an error envelope.
// There is no parse id or data to inspect here.
const body = (await res.json()) as ErrorEnvelope;
return {
kind: "request_error",
httpStatus: res.status,
type: body.error.type,
message: body.error.message,
requestId: body.error.request_id,
};
}
const body = (await res.json()) as ParseResult;
const validationErrors = body.validation_errors ?? [];
if (body.status === "completed" && validationErrors.length === 0) {
// Typed and schema-valid: safe to store directly.
return { kind: "valid", id: body.id, data: body.data };
}
// A 200 that still reports something worth a human look.
return { kind: "needs_review", id: body.id, data: body.data, validationErrors };
}
Because the schema contract is enforced server-side, the happy path never has to re-check types or
formats. Everything that comes back as valid already matches the schema you sent.
When to use raw MIME instead
Email-like text is the fastest way to prove the pipeline works and to iterate on a schema. Once you move to production traffic, send the full raw MIME message your mail provider gives you. You will want that when:
- You need real
From,To,Date, orMessage-IDheaders in the extraction context. - The email is multipart with both
text/plainandtext/htmlbodies. - The message carries attachments or other encoded parts. MailFrame separates them from the readable body, so encoded blobs never pollute the text handed to extraction.
- The message contains quoted reply threads and you want the parser to see the whole exchange.
Nothing else changes. The schema field, the response shape, and the error envelope are identical.
Only the string in raw_mime grows from a few lines to a full message.
Next steps
- Read Building a JSON Schema Email Parser for a deeper guide to applying schema validation to raw MIME email.
- The API reference covers every field on the parse request and the full error envelope.
- Try the Stripe receipt parser for a browser-only preview with no API key required.