Turn raw email into typed, schema-validated JSON. Start with the
five-minute quickstart, then use the reference below — every endpoint
ships with curl, TypeScript, and Python examples.
From signup to your first typed JSON in five steps. You will request access,
create an API key, pick a schema, send a raw email to /v1/parse, and validate the response.
1
Request access & create a key
MailFrame is rolling out to developers in batches.
Request early access,
then create an API key in the dashboard. The secret is shown once — store it in
your secret manager, never in source control.
# Store your key as an environment variable (never hard-code it)
export MAILFRAME_API_KEY="mf_live_xxxxxxxxxxxxxxxx"
2
Check connectivity
Hit the public health endpoint to confirm you can reach the API before wiring in auth.
curl https://api.mailframe.ai/health
const res = await fetch("https://api.mailframe.ai/health", {
method: "GET",
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/health",
)
data = res.json()
3
Pick a schema
A schema is a JSON Schema document describing the fields you want. Use one from the
schema library (like stripe_receipt) or
define your own. MailFrame validates every extraction against it before returning.
POST the raw RFC 822 MIME text and the schema name to /v1/parse.
MailFrame answers the same request with the validated result.
curl -X POST https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer $MAILFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
}'
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
}),
});
const data = await res.json();
import os
import requests
res = requests.post(
"https://api.mailframe.ai/v1/parse",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
json={
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
},
)
data = res.json()
5
Validate the response & store it
Check the HTTP status, then the parse status and
validation_errors on the body. When the parse completed cleanly, the
payload is already typed and schema-valid — write it straight to your database.
import os
import requests
res = requests.post(
"https://api.mailframe.ai/v1/parse",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
json={
"schema_id": "stripe_receipt",
"raw_mime": raw_mime_text,
},
)
res.raise_for_status()
body = res.json()
# Typed and schema-validated already — check status, then store.
if body["status"] == "completed" and not body.get("validation_errors"):
receipts.insert(body["data"])
else:
print("Parse status:", body["status"], "errors:", body.get("validation_errors"))
Authentication
All API requests are authenticated with a bearer token over HTTPS:
Authorization: Bearer $MAILFRAME_API_KEY
Create, name, scope, and rotate keys in the MailFrame dashboard. The secret is shown once at creation.
Keys are environment-scoped (for example mf_live_… and mf_test_…). Keep them out of source control and client-side code.
API-key management endpoints (/v1/api-keys) require a write-scoped API key. The onboarding endpoint (/v1/onboarding) uses a dashboard session instead — it is labelled accordingly below.
Base URL & conventions
The API base URL is https://api.mailframe.ai.
Requests and responses are JSON. Errors use standard HTTP status codes with a JSON body:
Invalid request — bad JSON, unknown schema, or malformed input.
401
Missing or invalid API key.
422
Parsed, but the result failed schema validation (see validation_errors).
429
Rate or plan limit reached — back off and retry.
5xx
Server error — safe to retry with backoff.
Core
The two endpoints you need to ship: a health probe and the synchronous parser.
GET/health Public
Service health
Liveness probe for the API. Returns 200 when the service is up. Safe to poll from uptime checks; no authentication required. A companion GET /health/ready reports readiness once the parse pipeline is accepting traffic.
curl https://api.mailframe.ai/health
const res = await fetch("https://api.mailframe.ai/health", {
method: "GET",
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/health",
)
data = res.json()
200 response
{
"status": "ok",
"version": "1.x"
}
POST/v1/parse API key
Parse an email
The primary endpoint. POST a raw email as raw_mime (raw RFC 822 MIME text) along with a schema_id to extract against — or send an inline schema instead. MailFrame validates the extraction against your schema and returns typed JSON synchronously in the same HTTP response.
Direct POST of raw MIME is the fully supported path today. PDF/image input (the file field) and forwarding to a unique inbox address are on the roadmap (see Inbound, below).
curl -X POST https://api.mailframe.ai/v1/parse \
-H "Authorization: Bearer $MAILFRAME_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
}'
const res = await fetch("https://api.mailframe.ai/v1/parse", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
}),
});
const data = await res.json();
import os
import requests
res = requests.post(
"https://api.mailframe.ai/v1/parse",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
json={
"schema_id": "stripe_receipt",
"raw_mime": "From: billing@stripe.com\r\nTo: user@example.com\r\nSubject: Your Stripe Receipt\r\n\r\nThank you for your payment of $29.99."
},
)
data = res.json()
const res = await fetch("https://api.mailframe.ai/v1/schemas", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/schemas",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Fetch the full JSON Schema document for a single custom schema available in your tenant account so you can inspect the exact fields, types, and validation rules MailFrame enforces.
const res = await fetch("https://api.mailframe.ai/v1/schemas/stripe_receipt", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/schemas/stripe_receipt",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Replace a custom schema you own. Provide the schema name and the full JSON Schema document as schema_json; subsequent parses validate against the updated definition.
const res = await fetch("https://api.mailframe.ai/v1/schemas/invoice_v1", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.delete(
"https://api.mailframe.ai/v1/schemas/invoice_v1",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Parse history & usage
Read back past parses for auditing and reconciliation, and check your usage against plan limits.
GET/v1/parses API key
List parses
Page through your recent parses. Useful for dashboards, reconciliation, and re-fetching results you did not persist at parse time.
const res = await fetch("https://api.mailframe.ai/v1/parses?limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/parses?limit=20",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
const res = await fetch("https://api.mailframe.ai/v1/parses/parse_8f2a1c", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/parses/parse_8f2a1c",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
GET/v1/usage/stats API key
Usage stats
Return your parse volume for the current billing period and your plan limits. Use it to surface usage in your own UI or to alert before you hit a cap.
const res = await fetch("https://api.mailframe.ai/v1/usage/stats", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/usage/stats",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Manage the API keys on your account programmatically — these endpoints are authenticated with an API key — or interactively in the MailFrame dashboard. Onboarding runs from the dashboard with a signed-in session.
GET/v1/api-keys API key
List API keys
List the API keys on your account with their prefixes, scope, and last-used timestamps. Secret key material is only ever shown once, at creation time — it is never returned here.
You can also create and rotate keys interactively at app.mailframe.ai.
const res = await fetch("https://api.mailframe.ai/v1/api-keys", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/api-keys",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
const res = await fetch("https://api.mailframe.ai/v1/api-keys/key_a1b2", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.delete(
"https://api.mailframe.ai/v1/api-keys/key_a1b2",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
POST/v1/onboarding Dashboard session
Complete onboarding
Completes the signed-in account's onboarding and provisions initial defaults after sign-up. Powers the dashboard's getting-started flow and is authenticated with a Supabase dashboard session, not an API key.
curl -X POST https://api.mailframe.ai/v1/onboarding
const res = await fetch("https://api.mailframe.ai/v1/onboarding", {
method: "POST",
});
const data = await res.json();
import os
import requests
res = requests.post(
"https://api.mailframe.ai/v1/onboarding",
)
data = res.json()
Webhooks
Asynchronous, signed delivery of parse results to your own endpoint. Each account has a single webhook configuration. Available in early access — the request shapes below are stable but may change before general availability. Early-access endpoints require a write-scoped API key.
GET/v1/webhooks Early access
Get webhook configuration
Return your account's webhook configuration — the destination URL, whether delivery is active, and when it was created and last updated. Webhooks are configured per account, not per resource. The signing secret you provided is stored but never returned here.
const res = await fetch("https://api.mailframe.ai/v1/webhooks", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Set the HTTPS endpoint that receives parse results, along with the signing secret MailFrame uses to sign deliveries. You provide the signing secret in the request and MailFrame stores it; it is never echoed back in this or the GET response. MailFrame signs every delivery with an HMAC over the raw body so you can verify authenticity (see Verifying webhook signatures, below).
Remove your account's webhook configuration. MailFrame stops delivering events; you can re-create it at any time by providing your endpoint URL and signing secret again.
const res = await fetch("https://api.mailframe.ai/v1/webhooks", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.delete(
"https://api.mailframe.ai/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
GET/v1/webhooks/attempts Early access
List delivery attempts
Inspect the delivery log for your webhook: response codes, timing, and retry state for each attempt. Use it to debug an endpoint that is returning non-2xx.
const res = await fetch("https://api.mailframe.ai/v1/webhooks/attempts?limit=20", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/webhooks/attempts?limit=20",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
GET/v1/webhooks/dead-letter Early access
List dead letters
After retries are exhausted, undelivered events move to the dead-letter queue. List them here to see what your endpoint missed while it was down.
const res = await fetch("https://api.mailframe.ai/v1/webhooks/dead-letter", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/webhooks/dead-letter",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
POST/v1/webhooks/dead-letter/{id}/replay Early access
Replay a dead letter
Re-deliver a dead-lettered event once your endpoint is healthy again. The replayed delivery is signed exactly like the original.
curl -X POST https://api.mailframe.ai/v1/webhooks/dead-letter/dl_9f8e/replay \
-H "Authorization: Bearer $MAILFRAME_API_KEY"
const res = await fetch("https://api.mailframe.ai/v1/webhooks/dead-letter/dl_9f8e/replay", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.post(
"https://api.mailframe.ai/v1/webhooks/dead-letter/dl_9f8e/replay",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Inboxes & inbound
Forward email to a unique MailFrame address instead of POSTing it yourself. Forwarded mail is received internally on MailFrame's provider endpoint (POST /v1/inbound, provider-authenticated) — you never call it directly. Available in early access — direct POST to /v1/parse remains the supported path today. Early-access endpoints require a write-scoped API key.
GET/v1/inboxes Early access
List inboxes
List your inbound addresses. Each inbox maps incoming mail to a schema, so forwarded email is parsed and delivered to your account's webhook automatically.
const res = await fetch("https://api.mailframe.ai/v1/inboxes", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.get(
"https://api.mailframe.ai/v1/inboxes",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
POST/v1/inboxes Early access
Create an inbox
Provision an inbound address on a domain you control, optionally with a fixed local part and a default schema. Forward (or auto-forward) matching email to it and MailFrame parses each message and delivers the typed result to your configured webhook.
Update an inbox's delivery webhook URL. Mail received after the update is delivered to the new endpoint; the inbox's address and default schema are fixed at creation.
const res = await fetch("https://api.mailframe.ai/v1/inboxes/ibx_7a6b", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.MAILFRAME_API_KEY}`,
},
});
const data = await res.json();
import os
import requests
res = requests.delete(
"https://api.mailframe.ai/v1/inboxes/ibx_7a6b",
headers={"Authorization": f"Bearer {os.environ['MAILFRAME_API_KEY']}"},
)
data = res.json()
Verifying webhook signatures
Every webhook delivery is signed so you can confirm it genuinely came from MailFrame
and was not tampered with in transit. Each request carries these headers:
Header
Description
X-MailFrame-Signature
Hex HMAC-SHA256 of ${timestamp}.${rawBody}.
X-MailFrame-Timestamp
Unix seconds when the delivery was signed. Reject if too old.
X-MailFrame-Event
Event type, e.g. parse.completed.
Recompute the HMAC over the raw request body (not re-serialized JSON)
with your endpoint's signing secret, and compare it to the header in constant time.
Reject deliveries whose timestamp is outside a tolerance window to blunt replay attacks.
import crypto from "node:crypto";
// MailFrame signs each delivery with an HMAC-SHA256 over
// "${timestamp}.${rawBody}" using your endpoint's signing secret.
// Headers: X-MailFrame-Signature, X-MailFrame-Timestamp, X-MailFrame-Event.
export function verifyMailFrame(
rawBody: string,
headers: Record<string, string>,
signingSecret: string,
toleranceSeconds = 300,
): boolean {
const signature = headers["x-mailframe-signature"];
const timestamp = headers["x-mailframe-timestamp"];
if (!signature || !timestamp) return false;
// Reject stale deliveries to blunt replay attacks.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (Number.isNaN(age) || age > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time comparison — never use === on secrets.
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
import hashlib
import hmac
import time
# MailFrame signs each delivery with an HMAC-SHA256 over
# f"{timestamp}.{raw_body}" using your endpoint's signing secret.
# Headers: X-MailFrame-Signature, X-MailFrame-Timestamp, X-MailFrame-Event.
def verify_mailframe(raw_body: bytes, headers, signing_secret: str, tolerance_seconds: int = 300) -> bool:
signature = headers.get("X-MailFrame-Signature")
timestamp = headers.get("X-MailFrame-Timestamp")
if not signature or not timestamp:
return False
# Reject stale deliveries to blunt replay attacks.
try:
if abs(time.time() - int(timestamp)) > tolerance_seconds:
return False
except ValueError:
return False
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(signing_secret.encode(), signed_payload, hashlib.sha256).hexdigest()
# Constant-time comparison — never use == on secrets.
return hmac.compare_digest(expected, signature)
Wiring it into an Express handler — note that you must read the raw body before any JSON parser runs:
import express from "express";
import { verifyMailFrame } from "./verify-mailframe";
const app = express();
// Capture the RAW body — signatures are computed over bytes, not parsed JSON.
app.post(
"/hooks/mailframe",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verifyMailFrame(
rawBody,
req.headers as Record<string, string>,
process.env.MAILFRAME_WEBHOOK_SECRET!,
);
if (!ok) return res.status(400).send("invalid signature");
const event = JSON.parse(rawBody);
// Acknowledge fast (2xx), then process out of band.
res.sendStatus(200);
void handleEvent(event);
},
);
The signing secret (whsec_…) is the value you provide when you configure your webhook and is distinct
from your API key. Store it as MAILFRAME_WEBHOOK_SECRET. Webhook delivery is in early access.
Status & uptime
Live component status, the incident policy, and update history are on the
MailFrame status page
(also served at status.mailframe.ai).
For programmatic checks, poll GET /health for liveness, or GET /health/ready once the parse pipeline is accepting traffic.
Frequently asked questions
What input can I send to MailFrame today?
Today you POST raw RFC 822 MIME text directly to /v1/parse and get typed JSON back synchronously. PDF and image input, plus forwarding to a unique inbox address and asynchronous webhook delivery, are in early access — direct POST of raw email is the fully supported path right now. You can try a live parse in the browser without an API key.
How do I authenticate?
Send your API key as a bearer token: Authorization: Bearer $MAILFRAME_API_KEY. Create and rotate keys in the MailFrame dashboard. The /v1/api-keys REST endpoints also work for programmatic key management, but they require a write-scoped API key to call. The /v1/onboarding endpoint uses a dashboard session instead of an API key.
How are results delivered?
MailFrame returns the validated result synchronously in the response to your POST /v1/parse request — typed JSON that matches your schema, along with the parse status and any schema validation errors. Asynchronous, signed webhook delivery is available in early access.
How do I verify a webhook came from MailFrame?
Each delivery carries X-MailFrame-Signature and X-MailFrame-Timestamp headers. Compute an HMAC-SHA256 over the signed payload with your endpoint's signing secret and compare it in constant time, rejecting stale deliveries. See the verification guide for ready-to-use TypeScript and Python.
Get your API key
MailFrame is rolling out to developers in batches. Request early access and we'll
send your keys when your spot opens.