Skip to content
ParsedMail

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

From: Priya Raman <[email protected]> Subject: Order 77120 arrived broken Hi, My order 77120 came yesterday and the glass carafe (Pour-over set, 1 L) was cracked in the box. Photos attached. I would like a refund rather than a replacement, please. Thanks, Priya > On Mon, Support wrote: > Thanks for shopping with us

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 pathScopePurpose
GET /usagereadPlan, emails and pages used in the current period
GET /inboxes, POST /inboxesread, writeList or create inboxes
POST /inboxes/{id}/emailsparseSubmit an email for asynchronous parsing, returns 202
GET /schemas, POST /schemas, PUT /schemas/{id}read, writeManage schemas; GET /schemas/{id} includes json_schema
GET /emailsreadFilter by status, inbox and date, cursor pagination
GET /emails/{id}readOne email with data and confidence
POST /emails/{id}/replayparseRe-parse a stored email, for example after a schema change
DELETE /emails/{id}writeDelete a stored email
POST /parseparseSynchronous 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.eml

The 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_schema

Feed 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:

  • 401 missing or invalid key, 403 forbidden key lacks the scope
  • 422 empty_email, invalid_schema or invalid_request for bad input
  • 402 plan_limit from POST /parse when the period quota is used up, and from POST /inboxes over the plan's inbox count; emails sent to an inbox are still accepted and stored as over_limit
  • 409 inbox_paused when you post to a paused inbox
  • 413 too_large, 410 purged for an email past its retention period
  • 429 rate limit and 503 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?

No. Every inbox has its own address on parsedmail.com, so you can forward mail there. On Scale you can also receive on your own subdomain with an MX record, for example [email protected] for the inbox [email protected].

Which formats can I post to an inbox?

Raw MIME with 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?

Yes. 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?

Keys are created on the API keys page in the dashboard. Each key has any of the scopes read, parse and write, so you can keep write access away from services that only submit mail.

Can I try the API before paying?

After you confirm your email address you get a one-time sandbox of 10 emails. Emails parsed there carry "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.