# Mark an email read or unread, starred or unstarred

Source: https://docs.emelia.io/docs/inbox-api/mark-read-or-starred

Guide: [Inbox API](https://docs.emelia.io/docs/inbox-api.md)

`PATCH https://api.emelia.io/inbox/api/v1/emails/{emailId}`

Provide `isRead`, `isStarred` or both, as booleans. A body with neither is rejected rather than treated as a no-op. The change is idempotent, pushed to open inbox windows in real time, propagated to the provider in the background for the read state (the star stays local), and delivered to `email.updated` webhooks.

Inconsistency: an unknown id returns `404 NOT_FOUND` here, while `GET /emails/{emailId}` returns `EMAIL_NOT_FOUND`.

## Authentication

Send your API key in the `Authorization` header: `Authorization: <your API key>`.

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `emailId` | string | yes | Inbox id of the email (from `GET /emails`, webhooks...). |

## Request body

Content type: `application/json`, required.

At least one of the two fields.

| Field | Type | Required | Description |
|---|---|---|---|
| `isRead` | boolean | no |  |
| `isStarred` | boolean | no |  |

```json
{
  "isRead": true
}
```

## Example request

**cURL**

```bash
curl -X PATCH "https://api.emelia.io/inbox/api/v1/emails/cmemacme0000000000000001" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "isRead": true
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/inbox/api/v1/emails/cmemacme0000000000000001', {
  method: 'PATCH',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "isRead": true
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.patch(
    "https://api.emelia.io/inbox/api/v1/emails/cmemacme0000000000000001",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "isRead": True
    },
)
print(response.json())
```

## Responses

### 200: The updated email.

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | object | yes | Full metadata of an email or a draft (`GET /emails/{emailId}`, `PATCH /emails/{emailId}`, drafts). Has `bcc` and provider `labels`, has no `tags`. Never contains the body. |
| `email.id` | string | yes | Inbox id of the email. |
| `email.mailboxId` | string | yes |  |
| `email.folderId` | string | yes |  |
| `email.messageId` | string | yes | RFC 5322 Message-ID header. Needed to reply in a thread. |
| `email.threadId` | string or null | yes | Provider conversation id (Gmail, Microsoft), null for IMAP. |
| `email.subject` | string | yes |  |
| `email.from` | object | yes | An address as returned by the API. `name` is null when unknown. |
| `email.to` | array of object | yes |  |
| `email.cc` | array of object | yes |  |
| `email.date` | string (date-time) | yes |  |
| `email.snippet` | string | yes | Short text preview (about 200 characters). |
| `email.isRead` | boolean | yes |  |
| `email.isStarred` | boolean | yes |  |
| `email.isDraft` | boolean | yes |  |
| `email.waitingForReply` | boolean | yes | Estimate that a sent email still expects an answer. |
| `email.hasAttachments` | boolean | yes |  |
| `email.attachments` | array of object | yes |  |
| `email.inReplyTo` | string or null | yes |  |
| `email.references` | array of string | yes |  |
| `email.bcc` | array of object | yes | Only known on sent messages and drafts. |
| `email.labels` | array of string | yes | Provider labels (for example Gmail `INBOX`, `UNREAD`, `IMPORTANT`), NOT inbox labels. |
| `email.isAutoReply` | boolean | yes | Sent automatically by an auto-reply. |
| `email.isAutoForward` | boolean | yes | Sent automatically by an auto-forward rule. |
| `email.scheduledStatus` | enum: `PENDING`, `QUEUED`, `SENDING`, `FAILED`, `null` | yes | For a scheduled draft: its sending state. |
| `email.scheduledSendAt` | string or null (date-time) | yes |  |

```json
{
  "email": {
    "id": "cmemacme0000000000000001",
    "mailboxId": "cmbxacme0000000000000001",
    "folderId": "cmfdacme0000000000000001",
    "messageId": "<CAJacme.0001@mail.example.com>",
    "threadId": "18c2acme0001",
    "subject": "Re: Partnership with Acme",
    "from": {
      "email": "jane.roe@example.com",
      "name": "Jane Roe"
    },
    "to": [
      {
        "email": "john.doe@acme.com",
        "name": null
      }
    ],
    "cc": [],
    "bcc": [],
    "date": "2026-10-08T09:12:44.000Z",
    "snippet": "Thanks John, could we schedule a call next week?",
    "isRead": true,
    "isStarred": false,
    "isDraft": false,
    "hasAttachments": true,
    "attachments": [
      {
        "filename": "brief.pdf",
        "size": 48213,
        "mimeType": "application/pdf"
      }
    ],
    "inReplyTo": "<20261008091500.acme01@acme.com>",
    "references": [
      "<20261008091500.acme01@acme.com>"
    ],
    "labels": [
      "INBOX"
    ],
    "waitingForReply": false,
    "isAutoReply": false,
    "isAutoForward": false,
    "scheduledStatus": null,
    "scheduledSendAt": null
  }
}
```

### 400: `VALIDATION_FAILED` when neither field is a boolean (`details.accepted` lists them), or `REQUEST_ERROR` on malformed JSON.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 401: Missing, malformed, unknown or disabled key. No `X-RateLimit-*` headers.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 403: `WORKSPACE_SUSPENDED`: the Emelia subscription has expired.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 404: `NOT_FOUND`: the email does not exist, or belongs to another workspace (never 403, to prevent enumeration).

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 413: `PAYLOAD_TOO_LARGE`: JSON body above 256 KB.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 429: `RATE_LIMIT_EXCEEDED`: per minute limit of the workspace (wait `Retry-After`). `QUOTA_EXCEEDED`: monthly quota exhausted until the 1st of next month UTC (in Emelia the plan cannot be upgraded).

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```

### 500: `INTERNAL_ERROR`: unexpected failure. The message is generic in production.

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | enum: `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, `FORBIDDEN`, `PLAN_REQUIRED`, `VALIDATION_FAILED`, `REQUEST_ERROR`, `PAYLOAD_TOO_LARGE`, `NOT_FOUND`, `EMAIL_NOT_FOUND`, `DRAFT_NOT_FOUND`, `MAILBOX_NOT_FOUND`, `LABEL_NOT_FOUND`, `VIEW_NOT_FOUND`, `RULE_NOT_FOUND`, `FILTER_NOT_FOUND`, `SIGNATURE_NOT_FOUND`, `MAILBOX_INACTIVE`, `INVALID_SENDER_ALIAS`, `NO_TRASH_FOLDER`, `INVALID_TAG`, `CONFLICT`, `DUPLICATE_TAG_NAME`, `DUPLICATE_RULE`, `DUPLICATE_FILTER`, `SCHEDULED_SEND_IN_PROGRESS`, `DRAFT_ALREADY_EXISTS`, `QUERY_TIMEOUT`, `RATE_LIMIT_EXCEEDED`, `QUOTA_EXCEEDED`, `INTERNAL_ERROR` | yes | Codes that the Inbox API can return, as found in the code. Branch on `code`, never on `error`. The HTTP status is derived from the code (unknown business codes are 400). |
| `error` | string | yes | Human readable. |
| `details` | object | no | Optional context: faulty fields, bounds, ids. |

```json
{
  "code": "INVALID_API_KEY",
  "error": "string",
  "details": {}
}
```
