Skip to content
ParsedMail

API reference

The ParsedMail REST API lives at https://parsedmail.com/api/v1 and speaks JSON. Authenticate with an API key in the Authorization: Bearer header. Endpoints cover usage, inboxes, schemas and parsed emails, plus a synchronous parse call; each key carries the scopes read, parse and write.

Authentication, scopes and rate limit

Create keys on the API keys page of the dashboard. A key is shown once; revoke it there when it leaks. Send it with every request. The Accept: application/json header in the examples is optional, because the API always answers in JSON:

curl https://parsedmail.com/api/v1/usage \
  -H "Authorization: Bearer $PARSEDMAIL_KEY" \
  -H "Accept: application/json"

A key belongs to the workspace you were in when you created it, and it keeps working on that workspace whichever workspace you open later in the dashboard. If the person who created the key leaves the workspace, the key stops working and every request answers 403; create a new key from someone who is still a member.

ScopeEndpoints
readAll GET endpoints except /usage
parsePOST /inboxes/{id}/emails, POST /emails/{id}/replay, POST /parse
writePOST /inboxes, POST /schemas, PUT /schemas/{id}, DELETE /emails/{id}
any scopeGET /usage

A missing scope answers 403 with type forbidden. The rate limit is 120 requests per minute per workspace, shared by all its keys. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with type rate_limited and a Retry-After header in seconds. Error bodies are listed in errors and limits.

Pagination and ids

Objects have prefixed ids: inb_ for inboxes, sch_ for schemas, eml_ for emails. GET /emails is paginated with a cursor, newest first:

ParameterTypeDescription
limitintegerItems per page, 1 to 100, default 20
starting_afteremail idReturn emails older than this one. Pass the last id of the previous page
has_moreboolean (response)true when another page exists
# first page
GET /v1/emails?limit=100
# next page
GET /v1/emails?limit=100&starting_after=eml_9vd0c2a7mm1k

An id in starting_after that does not belong to an email of your workspace answers 422 invalid_request, so take it from a previous response. An email deleted after you fetched the page still works as a cursor. GET /inboxes and GET /schemas return all items in one response.

Usage

GET /v1/usage, any scope. Consumption in the current monthly period. Plans billed yearly also have monthly limits. In the sandbox, plan is sandbox and the period fields are null.

{
  "plan": "growth",
  "emails_used": 1840,
  "emails_limit": 5000,
  "pages_used": 612,
  "pages_limit": 2000,
  "period_start": "2026-10-03",
  "period_end": "2026-11-03"
}

Inboxes

GET /v1/inboxes, scope read. Returns {"data": [inbox, ...]}.

GET /v1/inboxes/{id}, scope read. One inbox, or 404 not_found.

POST /v1/inboxes, scope write. Creates an inbox and returns it with 201. Over the plan's inbox count it answers 402 plan_limit.

ParameterTypeDescription
namestring, requiredUp to 60 characters. The address is built from it plus a short random suffix
schemaschema idOptional schema to attach. Unknown id: 404 not_found
curl https://parsedmail.com/api/v1/inboxes \
  -H "Authorization: Bearer $PARSEDMAIL_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Orders", "schema": "sch_vnf3updh2qin"}'
{
  "id": "inb_8k2m4q7d1x",
  "object": "inbox",
  "name": "Orders",
  "address": "[email protected]",
  "schema": "sch_vnf3updh2qin",
  "classify": false,
  "status": "active",
  "allowed_senders": [],
  "created_at": "2026-10-15T09:00:12+00:00"
}

status is active or paused. Allowed senders, classification, the review threshold and pausing are set in the dashboard, as described in inboxes and forwarding.

POST /v1/inboxes/{id}/emails, scope parse. Submits one email for asynchronous parsing and returns 202:

ParameterTypeDescription
bodyraw MIMEWith Content-Type: message/rfc822 or text/plain
rawstringJSON: the raw MIME message
emailstringJSON: pasted email text with optional From and Subject lines
from, from_name, subjectstringJSON: sender address, sender name, subject
text, htmlstringJSON: plain text and HTML body; one is enough
message_idstringJSON: Message-ID, used for duplicate detection
attachmentsarrayJSON: objects with filename, content_type and base64 content
{"id": "eml_f0qfttfnggyv5cn4a3ka", "object": "email", "status": "received"}

Errors: 404 not_found (inbox), 409 inbox_paused, 413 too_large (raw MIME over 25 MB, JSON body over about 33.4 MB), 422 empty_email, 422 invalid_request (attachment content that is not base64). Examples for every body shape are in sending emails.

Schemas

GET /v1/schemas, scope read. Returns {"data": [schema, ...]}.

GET /v1/schemas/{id}, scope read. One schema including its JSON Schema export.

{
  "id": "sch_vnf3updh2qin",
  "object": "schema",
  "name": "Invoices",
  "description": "invoice number, due date, total",
  "version": 2,
  "fields": [
    {"key": "invoice_number", "label": "Invoice number", "type": "string", "description": "", "required": true},
    {"key": "due_date", "label": "Due date", "type": "date", "description": "", "required": false},
    {"key": "total", "label": "Total", "type": "money", "description": "Total including tax", "required": true}
  ],
  "json_schema": {"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Invoices", "type": "object", "properties": {...}, "required": ["invoice_number", "total"]},
  "created_at": "2026-10-14T16:41:09+00:00"
}

POST /v1/schemas, scope write. Creates a schema and returns it with 201. Send fields, or a description to have the fields built from it.

ParameterTypeDescription
namestring, requiredUp to 80 characters
descriptionstringUp to 2,000 characters. Plain-English list of fields
fieldsarrayField objects: key or label, type, optional description, required, options (enum), columns (table). At most 60
curl https://parsedmail.com/api/v1/schemas \
  -H "Authorization: Bearer $PARSEDMAIL_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Support tickets",
    "fields": [
      {"label": "Customer email", "type": "email", "required": true},
      {"label": "Order number", "type": "string"},
      {"label": "Topic", "type": "enum", "options": ["refund", "delivery", "damaged item", "other"]},
      {"label": "Items", "type": "table", "columns": [{"label": "Product", "type": "string"}, {"label": "Quantity", "type": "integer"}]}
    ]
  }'

PUT /v1/schemas/{id}, scope write. Updates name, description and fields; returns the schema. When the fields change, version goes up by one. Fields are replaced only when you send fields, or description together with "regenerate": true; sending only name or description updates the text and keeps the fields and the version. A bad field definition answers 422 invalid_schema with the reason, for example "Enum field \"topic\" needs at least two options." Types and limits are in defining schemas.

Emails

GET /v1/emails, scope read. Lists emails of the workspace, newest first.

ParameterTypeDescription
statusstringOne of received, processing, parsed, needs_review, delivered, failed, duplicate, over_limit, blocked_sender, skipped
inboxinbox idOnly emails of this inbox
sinceISO 8601Only emails stored at or after this time, for example 2026-10-01T00:00:00Z. Invalid value: 422 invalid_request
limitinteger1 to 100, default 20
starting_afteremail idCursor from the previous page
curl "https://parsedmail.com/api/v1/emails?status=parsed&inbox=inb_8k2m4q7d1x&limit=2" \
  -H "Authorization: Bearer $PARSEDMAIL_KEY" \
  -H "Accept: application/json"
{
  "data": [
    {"id": "eml_f0qfttfnggyv5cn4a3ka", "object": "email", "status": "parsed", "data": {...}, ...},
    {"id": "eml_9vd0c2a7mm1k", "object": "email", "status": "parsed", "data": {...}, ...}
  ],
  "has_more": true
}

GET /v1/emails/{id}, scope read. One email object, the same document a webhook carries in data:

ParameterTypeDescription
id, objectstringeml_... and "email"
inbox, inbox_addressstringInbox id and address
schema, schema_versionstring, integerSchema used and its version; null before parsing
statusstringSee the status lifecycle
received_atISO 8601Date of the email, or arrival time when it has none
fromobject{"email", "name"}
tostringAddress the email was sent to, including any plus tag
subject, message_idstringSubject and Message-ID
duplicate_ofemail idEarlier email for status duplicate
attachmentsarray{"filename", "content_type", "size", "pages", "parsed"}
dataobjectExtracted values in schema order; corrected values after review
confidence, confidence_avgobject, numberScore 0 to 1 per field and the average
needs_review, reviewedbooleanWaiting for review; reviewed by a person
classificationobject{"schema", "name", "confidence"} when the inbox classifies, else null
warningsarray{"field", "message"} for values that could not be read
sandboxbooleantrue for emails parsed in the sandbox

POST /v1/emails/{id}/replay, scope parse. Parses the stored email again with the current schema version and returns 202 with {"id", "object", "status": "received"}. Raw email already deleted under retention: 410 purged.

DELETE /v1/emails/{id}, scope write. Removes the email from the log and the API. Deleting never gives back usage.

{"id": "eml_f0qfttfnggyv5cn4a3ka", "object": "email", "deleted": true}

Synchronous parse

POST /v1/parse, scope parse. Parses one email in the request and returns the result. Counts one email plus the attachment pages it reads, with the same attachment rules as inbox emails. Nothing is stored.

ParameterTypeDescription
schemaschema idSaved schema to use
fieldsarrayInline field definitions, used when schema is absent
descriptionstringPlain-English fields, used when schema and fields are absent
emailstringPasted email text
rawstringRaw MIME message
textstringPlain body, optionally with from, subject and html
attachmentsarrayObjects with filename, content_type and base64 content; also read from raw. An email with only attachments is accepted
curl https://parsedmail.com/api/v1/parse \
  -H "Authorization: Bearer $PARSEDMAIL_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"description": "tracking number, carrier, estimated delivery date", "text": "Your parcel 1Z999AA10123456784 ships with UPS and arrives Thursday, October 22, 2026."}'
{
  "object": "parse_result",
  "schema": null,
  "data": {"tracking_number": "1Z999AA10123456784", "carrier": "UPS", "estimated_delivery_date": "2026-10-22"},
  "confidence": {"tracking_number": 0.98, "carrier": 0.96, "estimated_delivery_date": 0.94},
  "confidence_avg": 0.96,
  "warnings": [],
  "fields": [
    {"key": "tracking_number", "label": "Tracking number", "type": "string", "description": "", "required": false},
    {"key": "carrier", "label": "Carrier", "type": "string", "description": "", "required": false},
    {"key": "estimated_delivery_date", "label": "Estimated delivery date", "type": "date", "description": "", "required": false}
  ],
  "pages_counted": 0,
  "attachments_skipped": []
}

pages_counted is the number of attachment pages the call used. attachments_skipped lists files that were not read, each as {"filename", "reason"}, for example a logo or, in the sandbox, a scanned PDF.

Errors: 404 not_found (schema), 422 invalid_schema, 422 empty_email, 422 invalid_request, 413 too_large, 402 plan_limit, 503 parser_unavailable.

Frequently asked questions

Is there an SDK?

No official SDK. The API is plain JSON over HTTPS, so any HTTP client works, and the JSON Schema export of each schema lets you generate types for your language.

Can one key work across several workspaces?

No. A key belongs to the workspace it was created in. Create one key in each workspace.

Can I create webhook endpoints through the API?

No. Webhook endpoints, allowed senders, classification and review settings are managed in the dashboard. Inboxes, schemas and emails are available in the API.

Can I delete or pause an inbox through the API?

No. Deleting and pausing inboxes happens in the dashboard. The API can create inboxes, read them and submit emails to them.

Related