> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parser-app.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Emails and threads

> Every inbound email gets a stable id, a record, its raw .eml and your tags. See a deal's whole thread, and erase it.

Every email Parser receives is tracked, on every path: a forwarding address, a Gmail, Microsoft 365 or IMAP
mailbox, or `POST /v1/intake/email` (signed by a mail provider, or with an API key). Ignored and refused emails are
tracked too, so your CRM can account for every message that came in, not only the ones that became jobs.

## The email id

Each email gets an id like `em_3f1c0e9a6b2d4c8e1a7f5b90`. It is **stable**: a redelivery of the same message (an
Email Worker retry, a provider pushing it again, a mailbox resync) is the same email, never a second one. The id
comes back:

* in the `X-Email-ID` header of every `POST /v1/intake/email` answer, and as `email_id` in an ignored email's body;
* as `email_id` in each row of `GET /v1/mailboxes/{id}/messages`;
* in a job's `intake.email.email_id`: the email that started the job (also in every job event);
* in the [`email.received`](/guides/webhooks#email-received) webhook.

## The record

`GET /v1/emails/{email_id}` (`jobs:read`):

```json theme={null}
{
  "email_id": "em_3f1c0e9a6b2d4c8e1a7f5b90",
  "thread_id": "4b6e…/deal-123@example-iso.com",
  "mailbox_id": "4b6e…",
  "provider_message_id": "deal-123@example-iso.com",
  "message_id": "deal-123@example-iso.com", "in_reply_to": null, "references": [],
  "from": "broker@example-iso.com", "to": ["submissions@funder.example"], "cc": [],
  "subject": "New deal: Blue Fern Bakery",
  "received_at": "2026-10-14T20:29:41Z",
  "size_bytes": 1048211,
  "attachments": [{"filename": "march.pdf", "content_type": "application/pdf", "size_bytes": 482113, "sha256": "…"}],
  "outcome": "job", "job_id": "…",
  "routing": {"intent": "new_submission", "confidence": 0.97, "model": "…", "served_model": "…"},
  "tags": ["client:lifetime"],
  "raw_stored": true
}
```

| Field | What it holds |
| - | - |
| `thread_id` | The thread: the mailbox and the thread's root message id (the first of `References`, else `In-Reply-To`, else its own). Without a mailbox (an API-key post), the root message id alone. Treat it as opaque. |
| `outcome` | `job` (it started one), `merged` (its documents were already in the thread's job), `ignored:<reason>` or `refused` (a paused mailbox, or a message over a size or attachment cap). |
| `job_id` | The job it started, or the thread's job when it arrived; null when there is none. |
| `attachments` | Every attachment, processable or not: name, declared type, size, SHA-256. Never the content. |
| `raw_stored` | Whether its raw `.eml` is kept (below). |

The body is never part of the record or of any event. `GET /v1/emails` lists emails newest first, filtered by
`mailbox_id`, `thread_id`, `tag`, `outcome` (`job`, `merged`, `ignored` for any reason, one `ignored:<reason>`,
`refused`), `sender`, `since` and `job_id`, paged with `cursor`.

## The raw .eml

`GET /v1/emails/{email_id}/raw` returns the message exactly as it was received, byte for byte, as
`message/rfc822` with `Content-Disposition: attachment`. It is stored as job files are, encrypted with your
tenant's own key.

| Email | Raw kept |
| - | - |
| `job`, `merged`, every `ignored:<reason>` (including routing skips) | Yes |
| `ignored:sender_not_allowed` (an outsider mailing a forwarding address with allowed senders) | No: its envelope only |
| `refused` (a paused mailbox, a message over a cap) | No: its envelope only |
| From a mailbox with `store_raw_email: false` | No: its envelope only |

`store_raw_email` is a mailbox setting, on by default:

```bash theme={null}
curl -sS -X PATCH "$API/v1/mailboxes/$MAILBOX_ID" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"settings": {"store_raw_email": false}}'
```

`PATCH` replaces the mailbox's whole `settings`: send the others you set too. An email posted with an API key
(no mailbox) keeps its raw message. Messages are capped at 64 MB on every path.

## Threads

A deal arrives as a thread: the submission, internal forwards, questions back, replies that add a statement.
`GET /v1/emails/threads/{thread_id}` lists every tracked email of the thread in the order they arrived, each
with its outcome and job:

```json theme={null}
{"thread_id": "4b6e…/deal-123@example-iso.com", "emails": [
  {"email_id": "em_…a", "subject": "New deal: Blue Fern Bakery", "outcome": "job", "job_id": "job-1", …},
  {"email_id": "em_…b", "subject": "Re: New deal: Blue Fern Bakery", "outcome": "job", "job_id": "job-2", …},
  {"email_id": "em_…c", "subject": "Fwd: New deal: Blue Fern Bakery", "outcome": "merged", "job_id": "job-2", …}
]}
```

Here the reply added the missing statement, so it started `job-2` with every document of the thread (`job-1`'s
`superseded_by` names it), and the forward brought nothing new. From a job, `intake.email.thread_id` is its
thread, and `GET /v1/emails?job_id=` lists the emails that point at it.

`GET /v1/emails/threads/{thread_id}/raw` downloads the thread's kept messages as one mbox file (`mboxrd`), in
order. A thread id contains `/`; put it in the path as it is.

## Tags

Tag emails with your own labels, e.g. `client:lifetime` or `stage:funded`. A tag is trimmed and lowercased, then
must be 1 to 64 of `a-z 0-9 _ : -`; an email has at most 20. Writing tags needs `mailboxes:manage`, reading them
`jobs:read`.

| Call | Does |
| - | - |
| `POST /v1/emails/{email_id}/tags` `{"tags": [...]}` | Adds tags (one it has is kept once). |
| `PUT /v1/emails/{email_id}/tags` `{"tags": [...]}` | Replaces its tags. |
| `DELETE /v1/emails/{email_id}/tags/{tag}` | Removes one. |
| `GET /v1/emails?tag=client:lifetime` | Lists the emails with a tag. |

A mailbox's `default_tags` setting tags every email it receives, as it arrives:

```bash theme={null}
curl -sS -X PATCH "$API/v1/mailboxes/$MAILBOX_ID" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"settings": {"default_tags": ["client:lifetime"]}}'
```

Every tag change is in the audit log (`email.tags`, with the tags the email ended with). An invalid tag, or one
past 20, is refused with `422`.

## Erasure and retention

* `DELETE /v1/emails/{email_id}` erases one email: its record, its raw `.eml` and its `email.received` events.
* `DELETE /v1/emails/threads/{thread_id}` erases every email of a thread the same way.
* `DELETE /v1/jobs/{job_id}` (and `DELETE /v1/jobs?end_user_id=`) erases, with the job, **the emails that fed
  it**: the emails whose job it is (the one that started it, those merged into it, those that arrived while it
  was the thread's job), and the thread's earlier emails carrying a document it holds (a reply's job gathers the
  thread's earlier attachments). So in the thread above, erasing `job-2` erases all three emails, while erasing
  only the superseded `job-1` erases the submission alone and leaves the reply and the forward with `job-2`.
* A tenant's `retention_days` erases emails older than it, with their raw messages, as it erases old jobs.

Erasing an email does not erase its job; erasing a job does not erase a later job of its thread. Erasures need
`jobs:delete` and are in the audit log (`email.delete`, `email.delete_thread`, `job.delete`). Another tenant's
email or thread answers `404`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.