Email parsing API
An email parsing API turns incoming email into structured data your code can use. With ParsedMail you create an inbox, describe the fields in plain English, then receive typed JSON by signed webhook or fetch it over REST at https://parsedmail.com/api/v1, with a confidence score for every field.
Email received
Fields described as
ticket subject, customer email, order number, product name, problem category (one of damaged, missing, wrong item, other), refund requested
JSON delivered to your webhook
{ "ticket_subject": "Order 77120 arrived broken", 0.99 "customer_email": "[email protected]", 0.99 "order_number": "77120", 0.97 "product_name": "Pour-over set, 1 L", 0.93 "problem_category": "damaged", 0.95 "refund_requested": true 0.96 }
What the API gives you
ParsedMail is built around three objects. An inbox (inb_...) has a real address such as [email protected] and a schema attached. A schema (sch_...) lists typed fields: string, number, integer, money, date, datetime, boolean, email, phone, url, enum, list and table, up to 60 fields. An email (eml_...) is one received message with its status, extracted data, per-field confidence and any warnings.
You can drive all of it from code. Mail arrives either by forwarding (a Gmail filter, an Outlook rule, a server alias or BCC) or by posting the message to the API yourself. Results leave by webhook or by polling. If you only need a single conversion without inboxes, the synchronous POST /parse endpoint described on parse email to JSON returns the result in the same request.
Endpoints at a glance
All requests go to https://parsedmail.com/api/v1 with Authorization: Bearer <key>. Keys carry scopes, so a reporting job can hold a read-only key while your ingestion service holds a parse key.
| Method and path | Scope | Purpose |
|---|---|---|
GET /usage | read | Plan, emails and pages used in the current period |
GET /inboxes, POST /inboxes | read, write | List or create inboxes |
POST /inboxes/{id}/emails | parse | Submit an email for asynchronous parsing, returns 202 |
GET /schemas, POST /schemas, PUT /schemas/{id} | read, write | Manage schemas; GET /schemas/{id} includes json_schema |
GET /emails | read | Filter by status, inbox and date, cursor pagination |
GET /emails/{id} | read | One email with data and confidence |
POST /emails/{id}/replay | parse | Re-parse a stored email, for example after a schema change |
DELETE /emails/{id} | write | Delete a stored email |
POST /parse | parse | Synchronous one-off parse |
The rate limit is 120 requests per minute per workspace. Above it you get a 429. The full parameter list is in the API reference.
Submit an email from your own system
If mail already reaches your servers, post it to an inbox instead of forwarding. The endpoint accepts raw MIME, a pasted text body, or structured fields with base64 attachments:
curl https://parsedmail.com/api/v1/inboxes/inb_8Kq2/emails \
-H "Authorization: Bearer $PARSEDMAIL_KEY" \
-H "Content-Type: message/rfc822" \
--data-binary @order-77120.emlThe response is 202 {"id": "eml_...", "object": "email", "status": "received"}. Parsing continues in the background and the result is delivered to your webhook. In Node the same call with the structured body looks like this:
const res = await fetch("https://parsedmail.com/api/v1/inboxes/inb_8Kq2/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PARSEDMAIL_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "[email protected]",
subject: "Order 77120 arrived broken",
text: emailBody,
message_id: "<[email protected]>",
}),
});
const { id, status } = await res.json();Passing message_id lets deduplication work: the same Message-ID in the same inbox within 30 days is stored as duplicate, not parsed and not charged.
Read results by polling in Python
Webhooks are the usual path, but a batch job can also page through results. GET /emails takes status, inbox, since, limit (1 to 100) and starting_after as a cursor:
import os, requests
API = "https://parsedmail.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PARSEDMAIL_KEY']}"}
params = {"inbox": "inb_8Kq2", "status": "delivered", "limit": 100}
while True:
page = requests.get(f"{API}/emails", headers=HEADERS, params=params).json()
for email in page["data"]:
save_ticket(email["id"], email["data"], email["confidence_avg"])
if not page["has_more"]:
break
params["starting_after"] = page["data"][-1]["id"]Each email carries status, so you can skip anything still in needs_review and pick it up after a reviewer approves it in the human review queue.
Webhooks instead of polling
On paid plans you add HTTPS endpoints per workspace, or per inbox. Every email.parsed event is a POST with a JSON body {"id": "evt_...", "type": "email.parsed", "created": "...", "data": {...}}, where data is the same email object the REST API returns. Requests are signed with HMAC-SHA256 in the ParsedMail-Signature header and carry an Idempotency-Key. Anything other than a 2xx response is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. Verification code and payload details are on the inbound email webhook page.
Schemas you can version and validate
A schema starts as a sentence such as "ticket subject, customer email, order number, refund requested" and becomes typed fields you can edit. Every change increments the schema version, and each email records the schema_version it was parsed with, so downstream code can tell old results from new ones. Every schema also exports as JSON Schema draft 2020-12:
curl https://parsedmail.com/api/v1/schemas/sch_3Fd9 \
-H "Authorization: Bearer $PARSEDMAIL_KEY" | jq .json_schemaFeed that into your validator, generate types from it, or check it into your repository next to the consumer code. When you change a field, POST /emails/{id}/replay re-parses stored mail with the new version, and a replay of an already counted email is not counted again.
Errors and limits you should handle
Errors share one shape, {"error": {"type": "...", "message": "..."}}. The ones worth handling in code:
401missing or invalid key,403 forbiddenkey lacks the scope422 empty_email,invalid_schemaorinvalid_requestfor bad input402 plan_limitfromPOST /parsewhen the period quota is used up, and fromPOST /inboxesover the plan's inbox count; emails sent to an inbox are still accepted and stored asover_limit409 inbox_pausedwhen you post to a paused inbox413 too_large,410 purgedfor an email past its retention period429rate limit and503 parser_unavailable, both safe to retry with backoff
Raw email is kept 7 days in the sandbox, 30 on Starter, 90 on Growth and up to 365 days or zero retention on Scale. Extracted JSON stays. Plans and quotas are on the pricing page.
Frequently asked questions
Do I need my own mail server to use the API?
[email protected] for the inbox [email protected].
Which formats can I post to an inbox?
Content-Type: message/rfc822, JSON with a raw field, JSON with pasted email text, or structured fields (from, subject, text, html, message_id) with base64 attachments.
Is there a synchronous endpoint?
POST /parse takes a schema id, a field list or a plain description plus the email, and returns data, confidence and warnings in the response. It counts as one email.
How do API keys and scopes work?
Can I try the API before paying?
"sandbox": true. There is no free plan.Related
See your own email as JSON
Paste one into the demo, describe the fields and look at the result. No account needed.