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.
| Scope | Endpoints |
|---|---|
read | All GET endpoints except /usage |
parse | POST /inboxes/{id}/emails, POST /emails/{id}/replay, POST /parse |
write | POST /inboxes, POST /schemas, PUT /schemas/{id}, DELETE /emails/{id} |
| any scope | GET /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:
| Parameter | Type | Description |
|---|---|---|
limit | integer | Items per page, 1 to 100, default 20 |
starting_after | email id | Return emails older than this one. Pass the last id of the previous page |
has_more | boolean (response) | true when another page exists |
# first page
GET /v1/emails?limit=100
# next page
GET /v1/emails?limit=100&starting_after=eml_9vd0c2a7mm1kAn 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.
| Parameter | Type | Description |
|---|---|---|
name | string, required | Up to 60 characters. The address is built from it plus a short random suffix |
schema | schema id | Optional 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:
| Parameter | Type | Description |
|---|---|---|
body | raw MIME | With Content-Type: message/rfc822 or text/plain |
raw | string | JSON: the raw MIME message |
email | string | JSON: pasted email text with optional From and Subject lines |
from, from_name, subject | string | JSON: sender address, sender name, subject |
text, html | string | JSON: plain text and HTML body; one is enough |
message_id | string | JSON: Message-ID, used for duplicate detection |
attachments | array | JSON: 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.
| Parameter | Type | Description |
|---|---|---|
name | string, required | Up to 80 characters |
description | string | Up to 2,000 characters. Plain-English list of fields |
fields | array | Field 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.
| Parameter | Type | Description |
|---|---|---|
status | string | One of received, processing, parsed, needs_review, delivered, failed, duplicate, over_limit, blocked_sender, skipped |
inbox | inbox id | Only emails of this inbox |
since | ISO 8601 | Only emails stored at or after this time, for example 2026-10-01T00:00:00Z. Invalid value: 422 invalid_request |
limit | integer | 1 to 100, default 20 |
starting_after | email id | Cursor 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:
| Parameter | Type | Description |
|---|---|---|
id, object | string | eml_... and "email" |
inbox, inbox_address | string | Inbox id and address |
schema, schema_version | string, integer | Schema used and its version; null before parsing |
status | string | See the status lifecycle |
received_at | ISO 8601 | Date of the email, or arrival time when it has none |
from | object | {"email", "name"} |
to | string | Address the email was sent to, including any plus tag |
subject, message_id | string | Subject and Message-ID |
duplicate_of | email id | Earlier email for status duplicate |
attachments | array | {"filename", "content_type", "size", "pages", "parsed"} |
data | object | Extracted values in schema order; corrected values after review |
confidence, confidence_avg | object, number | Score 0 to 1 per field and the average |
needs_review, reviewed | boolean | Waiting for review; reviewed by a person |
classification | object | {"schema", "name", "confidence"} when the inbox classifies, else null |
warnings | array | {"field", "message"} for values that could not be read |
sandbox | boolean | true 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.
| Parameter | Type | Description |
|---|---|---|
schema | schema id | Saved schema to use |
fields | array | Inline field definitions, used when schema is absent |
description | string | Plain-English fields, used when schema and fields are absent |
email | string | Pasted email text |
raw | string | Raw MIME message |
text | string | Plain body, optionally with from, subject and html |
attachments | array | Objects 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.