Sending emails to the API
To send an email to ParsedMail from your own system, POST it to /v1/inboxes/{id}/emails as raw MIME or as JSON. The API returns 202 with an email id at once, parses in the background and delivers the result by webhook. For a direct answer in the same request, use POST /v1/parse.
Submit a raw MIME message
If you have the original message, for example an .eml file or the source from your mail store, send it unchanged. Headers, the HTML part and attachments are read the same way as forwarded mail. The key needs the parse scope.
curl https://parsedmail.com/api/v1/inboxes/inb_8k2m4q7d1x/emails \
-H "Authorization: Bearer $PARSEDMAIL_KEY" \
-H "Accept: application/json" \
-H "Content-Type: message/rfc822" \
--data-binary @order-48213.emlHTTP/1.1 202 Accepted
{"id": "eml_f0qfttfnggyv5cn4a3ka", "object": "email", "status": "received"}A body sent with Content-Type: text/plain is also read as a raw MIME message. Use --data-binary, not -d, so line breaks survive.
Submit JSON instead
With Content-Type: application/json there are three shapes. The raw message as a string:
{"raw": "From: Northwind Supply <[email protected]>\r\nSubject: Order #NW-48213 confirmed\r\nMessage-ID: <[email protected]>\r\n\r\nOrder number: NW-48213 ..."}Pasted text, as a person would copy it from a mail client. From:, To:, Subject: and Date: lines at the top are recognized:
{"email": "From: [email protected]\nSubject: Order #NW-48213 confirmed\n\nOrder number: NW-48213\nTotal: $500.50"}Or separate fields, which is the easiest shape when the message is already in your database:
{
"from": "[email protected]",
"from_name": "Halcyon Cloud Billing",
"subject": "Invoice INV-2026-0917",
"text": "Please find attached invoice INV-2026-0917, due November 14, 2026.",
"html": "<p>Please find attached invoice <b>INV-2026-0917</b>.</p>",
"message_id": "<[email protected]>",
"attachments": [
{
"filename": "INV-2026-0917.pdf",
"content_type": "application/pdf",
"content": "JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI..."
}
]
}Only one of text and html is needed; an HTML-only body is converted to text with table rows kept. message_id is optional but makes duplicate detection exact. An email with no text and no attachments is refused with 422 empty_email. A paused inbox refuses new emails with 409 inbox_paused; resume it in the dashboard and send them again.
Attachments and size limit
In JSON, attachment content is standard base64 of the file bytes. Content that is not valid base64 is refused with 422 invalid_request and a message naming the attachment, for example attachments[0].content must be base64 encoded file content. One email can be up to 25 MB including attachments. A raw MIME body (message/rfc822) may be up to 25 MB; a JSON body may carry 25 MB of files after base64, which is a request body of about 33.4 MB. Larger requests get 413 too_large.
# Node: build the attachment from a file
const content = fs.readFileSync("INV-2026-0917.pdf").toString("base64");
# Python
content = base64.b64encode(open("INV-2026-0917.pdf", "rb").read()).decode()Up to 20 attachments are stored with an email and up to 8 files are read. PDFs with a text layer, CSV, TXT, HTML, ICS, JSON and XML are read as text; scanned PDFs and images are read as images on paid plans; small images such as logos and signature icons are skipped. A PDF page counts as one page on your plan and text files count one page per about 3,000 characters. Each attachment in the result says whether it was read:
"attachments": [
{"filename": "INV-2026-0917.pdf", "content_type": "application/pdf", "size": 48211, "pages": 2, "parsed": true},
{"filename": "logo.png", "content_type": "image/png", "size": 3120, "pages": 0, "parsed": false}
]More on file types is in parsing email attachments.
Status lifecycle
Every email has one status. Poll GET /v1/emails/{id} or, better, let a webhook tell you when it is done.
| Status | Meaning | Counted |
|---|---|---|
received | Stored and queued. Also shown while a temporary parsing error is retried automatically | No |
processing | Being read now | No |
parsed | JSON ready; webhook deliveries are queued | Yes |
needs_review | Waiting in the review queue (Growth, Scale) | Yes |
delivered | At least one webhook endpoint answered 2xx | Yes |
failed | Not parsed: inbox paused, no schema, parsing error after retries, or rejected in review | Only if it was parsed before rejection |
duplicate | Same message seen in the last 30 days; see duplicate_of | No |
over_limit | Plan or daily safety limit reached; stored, not parsed | No |
blocked_sender | Sender is not on the inbox allowed list | No |
skipped | Forwarding confirmation from Gmail or Outlook | No |
Submitting to an inbox never fails because of your plan limit; the email is accepted with 202 and then gets status over_limit. A temporary parsing error is retried automatically, up to 6 attempts over about 1 hour 45 minutes, before the status becomes failed.
Duplicate detection
The same message often arrives twice: a forward plus a CC, a retry in your own code, two rules matching one email. Within one inbox, an email is a duplicate when it has the same Message-ID as an earlier one, or, without a Message-ID, the same sender, subject and body. Earlier emails count for 30 days, and only when they were not themselves failed, over limit, blocked or duplicates.
{"id": "eml_9vd0c2a7mm1k", "object": "email", "status": "duplicate", "duplicate_of": "eml_f0qfttfnggyv5cn4a3ka", "data": null, ...}Duplicates are not parsed, not counted and not sent to webhooks. Since a resubmission after over_limit or failed is not a duplicate, you can simply send those emails again, though replaying them is cheaper on bandwidth.
Replay a stored email
Replay parses a stored email again with the inbox's current schema version. Use it after you change a schema, attach a missing schema, resume a paused inbox or move to a higher plan.
curl -X POST https://parsedmail.com/api/v1/emails/eml_f0qfttfnggyv5cn4a3ka/replay \
-H "Authorization: Bearer $PARSEDMAIL_KEY" \
-H "Accept: application/json"HTTP/1.1 202 Accepted
{"id": "eml_f0qfttfnggyv5cn4a3ka", "object": "email", "status": "received"}The email keeps its id and the new result replaces the old one, including any corrections made in review. A replay of an email that was already counted is not counted again; one that was never parsed, for example over_limit, counts once when it is parsed. Once the raw email is deleted under your retention setting, replay answers 410 purged.
Parse synchronously with POST /v1/parse
When you need the JSON in the same request, for example inside a support tool, call the parse endpoint. It needs no inbox. Pass a saved schema id, a fields array or a plain-English description, plus the email as email (pasted text), raw (MIME) or the JSON fields above, including base64 attachments.
curl https://parsedmail.com/api/v1/parse \
-H "Authorization: Bearer $PARSEDMAIL_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"schema": "sch_vnf3updh2qin", "email": "Subject: Invoice 0917\n\nInvoice number: 0917\nDue: November 14, 2026\nTotal: EUR 756.00"}'{
"object": "parse_result",
"schema": "sch_vnf3updh2qin",
"data": {"invoice_number": "0917", "due_date": "2026-11-14", "total": {"amount": 756, "currency": "EUR"}},
"confidence": {"invoice_number": 0.97, "due_date": 0.95, "total": 0.98},
"confidence_avg": 0.967,
"warnings": [],
"fields": [{"key": "invoice_number", "label": "Invoice number", "type": "string", "description": "", "required": false}, ...],
"pages_counted": 0,
"attachments_skipped": []
}Each call counts as one email. Attachments, from the raw MIME or from attachments in JSON, are read with the same rules as inbox emails: the first 8 files, attachment pages counted on paid plans, and in the sandbox only files of one page and no scans. An email that consists only of attachments is accepted. pages_counted tells you how many pages the call used, and attachments_skipped lists files that were not read, each with filename and reason:
"attachments_skipped": [{"filename": "logo.png", "reason": "Small inline image (logo or icon)"}]Nothing is stored, and there is no duplicate detection, review or webhook. A plan at its limit, including the page limit, answers 402 plan_limit, and a temporary parser problem 503 parser_unavailable, which you can retry. All error types are in errors and limits.
Frequently asked questions
Should I forward mail or use the API?
Is there a batch endpoint?
Does a duplicate count toward my plan?
How do I know when an asynchronous email is parsed?
email.parsed, or poll GET /v1/emails/{id} until the status is parsed, needs_review, delivered or failed.