# Inbox

Your team's support conversations: open them from your app, read and search them, reply from the channel's address, and keep internal notes. Start with the [Inbox API guide](https://www.rasket.com/docs/inbox-api) if this is your first call.

## Shared channels only

Every call reaches conversations in your team's open shared channels, such as `support@`. A personal mailbox belongs to its person: no key, token or agent can reach it, and its conversation IDs answer `404`, the same as an ID that does not exist.

## Inbox access

A key's Inbox access is set apart from its permission, and every key starts at `none` — a Full access key included. A team admin raises it in the dashboard under API keys, and can limit the key to some channels. A restricted channel is reachable only by a key limited to it.

| inbox_access | Can |
| --- | --- |
| `none` | The default, on every key. The key cannot reach the Inbox. |
| `create` | Open conversations only: POST /inbox/conversations and nothing else. |
| `read` | List, read and search conversations, messages and notes. |
| `write` | Read and reply: also reply, add notes, assign, and set status, labels and folders. |

An OAuth token needs `inbox:read` for reads and `inbox:write` for writes; one does not imply the other.

## A key is your app, not a person

- A reply from a key goes out from the channel's own name and address. A note from a key shows the key's name, and keeps it after the key is revoked.
- Read marks, stars, snoozes and drafts are one person's, so a key has none. Every list shows the team's view.
- A token or a session acts as its member, with that member's channels and role. A viewer reads and cannot write.

> `/inbox` request and response bodies are not kept in your request logs, because they hold your customers' mail. The method, path, status and timing are.

## Refusals worth knowing

| Status | Means |
| --- | --- |
| `403 inbox_not_set_up` | Your project has no open channel yet. Create one in Rasket Inbox. |
| `403 invalid_permission` | The key has no Inbox access, or not enough for this call; or the token lacks the scope. |
| `404 not_found` | No such channel or conversation, or one this credential cannot reach — a personal mailbox included. |
| `409 conversation_conflict` | A teammate is replying, a newer message arrived, or the conversation is in Spam or the Trash. |
| `412 precondition_failed` | The If-Match version is no longer current. |
| `422 snooze_needs_a_person` | An API key tried to snooze. Use status pending. |

The whole vocabulary is on [Errors](https://www.rasket.com/docs/errors). Inbox webhook events are on [Events](https://www.rasket.com/docs/events).

## Endpoints

### `GET /inbox/channels`

The shared channels this credential can reach, with their addresses.

List Inbox channels:

```sh
curl -X GET "https://api.rasket.com/inbox/channels" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/channels", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/channels",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_channel",
      "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
      "address": "support@lumen.app",
      "name": "Lumen Support",
      "restricted": false
    }
  ]
}
```

- Personal mailboxes are never listed. A restricted channel is listed only for a key limited to it.
- A project with no open channel answers `403 inbox_not_set_up` on every `/inbox` route.

### `GET /inbox/channels/{channel_id}/teammates`

The teammates who can open this channel, by name.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `channel_id` (required) | string | The channel's ID, from `GET /inbox/channels`. |

List who can be assigned in a channel:

```sh
curl -X GET "https://api.rasket.com/inbox/channels/0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20/teammates" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/channels/0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20/teammates", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/channels/0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20/teammates",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_teammate",
      "user_id": "0192a001-5b6c-7d8e-9f01-23456789abcd",
      "name": "Dana Ortiz",
      "can_write": true
    }
  ]
}
```

- `can_write` is false for a viewer, who reads the channel and cannot be assigned. Teammates are named, never addressed.

### `GET /inbox/labels`

The team's labels. A person's own labels are never listed.

List labels:

```sh
curl -X GET "https://api.rasket.com/inbox/labels" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/labels", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/labels",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_label",
      "id": "0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b",
      "name": "Billing",
      "colour": "blue",
      "channel_id": null
    }
  ]
}
```

### `GET /inbox/folders`

The team's folders. A person's own folders are never listed.

List folders:

```sh
curl -X GET "https://api.rasket.com/inbox/folders" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/folders", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/folders",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_folder",
      "id": "0192f6b3-2e5f-7a71-9bac-1d2e3f4a5b6c",
      "name": "Refunds",
      "parent_id": null,
      "channel_id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20"
    }
  ]
}
```

### `GET /inbox/conversations`

Conversations in the channels you can reach, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `channel_id` | string | Only conversations in this channel. |
| `status` | string | `open`, `pending` or `done`. |
| `assignee_id` | string | Only conversations assigned to this teammate, or `none` for unassigned ones. |
| `external_id` | string | The conversation your app opened with this `external_id`. |

#### Query parameters, less common

| Field | Type | Description |
| --- | --- | --- |
| `label_id` | string | Only conversations carrying this team label. |
| `folder_id` | string | Only conversations filed in this team folder. |
| `place` | string | `inbox` (the default; includes conversations filed in a folder), `archive`, `spam` or `trash`. With `external_id` and no `place`, every place is searched. |

List conversations:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations?status=open&limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations?status=open&limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations?status=open&limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_conversation",
      "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
      "channel": {
        "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
        "address": "support@lumen.app"
      },
      "subject": "Can I move my plan to annual?",
      "status": "open",
      "assignee": null,
      "labels": [
        {
          "id": "0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b",
          "name": "Billing",
          "colour": "blue"
        }
      ],
      "place": "inbox",
      "folder_id": null,
      "origin": "app",
      "external_id": "ticket_58213",
      "metadata": {
        "user_id": "usr_8412",
        "plan": "Team"
      },
      "customer": {
        "email": "priya@harborcoffee.co",
        "name": "Priya Raman",
        "contact_id": null,
        "recent_emails": null
      },
      "latest_from": "Priya Raman <priya@harborcoffee.co>",
      "snippet": "Hi! We love Lumen. Can we switch to yearly billing?",
      "message_count": 1,
      "last_message_at": "2026-10-04T09:03:12.000Z",
      "last_inbound_at": "2026-10-04T09:03:12.000Z",
      "created_at": "2026-10-04T09:03:12.000Z",
      "updated_at": "2026-10-04T09:03:12.000Z"
    }
  ]
}
```

- Filters combine. A row has no messages; retrieve the conversation for those.
- Every list shows the team's view: no read marks, stars or snoozes, which are one person's.

### `POST /inbox/conversations`

Open a conversation from your app, such as a Contact us form.

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | 1–256 characters, unique to this send. Replaying it inside 24 hours returns the original response instead of sending again. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `channel` (required) | string | A channel ID, or its full address such as `support@lumen.app`. |
| `from` (required) | object | `{ email, name }`: the customer. Replies go to this address. Rasket does not check it, and the conversation says so. |
| `subject` (required) | string | One line, up to 998 characters. |
| `text` | string | The message. Send `text`, `html` or both; up to 256 KB together. |
| `html` | string | The message as HTML. |
| `external_id` | string | Your own ID for this conversation, unique in your project. Sending one that already exists adds this message to that conversation and reopens it. Nothing else joins two messages from your app: not the sender, not the subject. |
| `metadata` | object | Up to 20 keys, each value a string of up to 500 characters. Shown to your team beside the conversation; replaced when you send it again. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `attachments` | object[] | Up to 10 files, 10 MB in total, each `{ filename, content, content_type }` with `content` base64-encoded. A URL is never fetched. |

Create a conversation:

```sh
curl -X POST "https://api.rasket.com/inbox/conversations" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: contact-58213" \
  -d '{
  "channel": "support@lumen.app",
  "from": {
    "email": "priya@harborcoffee.co",
    "name": "Priya Raman"
  },
  "subject": "Can I move my plan to annual?",
  "text": "Hi! We love Lumen. Can we switch to yearly billing?",
  "external_id": "ticket_58213",
  "metadata": {
    "user_id": "usr_8412",
    "plan": "Team"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
    "Idempotency-Key": "contact-58213",
  },
  body: JSON.stringify({
    channel: "support@lumen.app",
    from: {
      email: "priya@harborcoffee.co",
      name: "Priya Raman"
    },
    subject: "Can I move my plan to annual?",
    text: "Hi! We love Lumen. Can we switch to yearly billing?",
    external_id: "ticket_58213",
    metadata: {
      user_id: "usr_8412",
      plan: "Team"
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/inbox/conversations",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
        "Idempotency-Key": "contact-58213",
    },
    json={
    "channel": "support@lumen.app",
    "from": {
      "email": "priya@harborcoffee.co",
      "name": "Priya Raman"
    },
    "subject": "Can I move my plan to annual?",
    "text": "Hi! We love Lumen. Can we switch to yearly billing?",
    "external_id": "ticket_58213",
    "metadata": {
      "user_id": "usr_8412",
      "plan": "Team"
    }
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "inbox_conversation",
  "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "channel": {
    "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
    "address": "support@lumen.app"
  },
  "status": "open",
  "origin": "app",
  "external_id": "ticket_58213",
  "message_id": "0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5",
  "created": true
}
```

- `201` opens a conversation. `200` with `created: false` means `external_id` named one that exists, and the message was added to it.
- Needs Inbox access `create` (Open conversations only) or `write` (Read and reply), or `inbox:write` on a token.
- An unknown channel, or one the key may not use, is `404`; personal mailboxes never are usable. An `external_id` that belongs to another channel is `422`.
- It is received mail, and free. It never appears on `/emails/receiving` and fires no `email.received`; it fires `inbox.conversation.created` (or `inbox.message.received` on an append).
- Each credential may open 500 conversations or messages an hour; past that it is `429`.

### `GET /inbox/search`

Search with the Inbox's own grammar.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `q` (required) | string | Words, and `from:`, `to:`, `subject:`, `label:`, `mailbox:`, `in:`, `has:attachment`, `before:` and `after:`. Without `in:`, the Inbox and the Archive are searched. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |

Search conversations:

```sh
curl -X GET "https://api.rasket.com/inbox/search?q=refund+from%3Apriya%40harborcoffee.co" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/search?q=refund+from%3Apriya%40harborcoffee.co", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/search?q=refund+from%3Apriya%40harborcoffee.co",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_conversation",
      "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
      "channel": {
        "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
        "address": "support@lumen.app"
      },
      "subject": "Can I move my plan to annual?",
      "status": "open",
      "assignee": null,
      "labels": [
        {
          "id": "0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b",
          "name": "Billing",
          "colour": "blue"
        }
      ],
      "place": "inbox",
      "folder_id": null,
      "origin": "app",
      "external_id": "ticket_58213",
      "metadata": {
        "user_id": "usr_8412",
        "plan": "Team"
      },
      "customer": {
        "email": "priya@harborcoffee.co",
        "name": "Priya Raman",
        "contact_id": null,
        "recent_emails": null
      },
      "latest_from": "Priya Raman <priya@harborcoffee.co>",
      "snippet": "Hi! We love Lumen. Can we switch to yearly billing?",
      "message_count": 1,
      "last_message_at": "2026-10-04T09:03:12.000Z",
      "last_inbound_at": "2026-10-04T09:03:12.000Z",
      "created_at": "2026-10-04T09:03:12.000Z",
      "updated_at": "2026-10-04T09:03:12.000Z",
      "highlight": "can we get a <b>refund</b> for October"
    }
  ]
}
```

- `is:starred`, `is:unread` and `is:read` are one person's and answer `422`.
- `highlight` is HTML-escaped, with the hit in `<b>`.

### `GET /inbox/conversations/{conversation_id}`

One conversation, its newest messages and the customer.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

Retrieve a conversation:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "inbox_conversation",
  "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "channel": {
    "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
    "address": "support@lumen.app"
  },
  "subject": "Can I move my plan to annual?",
  "status": "open",
  "assignee": null,
  "labels": [
    {
      "id": "0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b",
      "name": "Billing",
      "colour": "blue"
    }
  ],
  "place": "inbox",
  "folder_id": null,
  "origin": "app",
  "external_id": "ticket_58213",
  "metadata": {
    "user_id": "usr_8412",
    "plan": "Team"
  },
  "customer": {
    "email": "priya@harborcoffee.co",
    "name": "Priya Raman",
    "contact_id": null,
    "recent_emails": null
  },
  "latest_from": "Priya Raman <priya@harborcoffee.co>",
  "snippet": "Hi! We love Lumen. Can we switch to yearly billing?",
  "message_count": 1,
  "last_message_at": "2026-10-04T09:03:12.000Z",
  "last_inbound_at": "2026-10-04T09:03:12.000Z",
  "created_at": "2026-10-04T09:03:12.000Z",
  "updated_at": "2026-10-04T09:03:12.000Z",
  "messages": [
    {
      "object": "inbox_message",
      "id": "0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5",
      "direction": "inbound",
      "from": "Priya Raman <priya@harborcoffee.co>",
      "to": ["support@lumen.app"],
      "cc": [],
      "subject": "Can I move my plan to annual?",
      "occurred_at": "2026-10-04T09:03:12.000Z",
      "text": "Hi! We love Lumen. Can we switch to yearly billing?",
      "html": null,
      "body_unavailable": false,
      "attachments": [],
      "received_email_id": "0192f8e1-7a4f-7b20-a1c3-6d5e4f3a2b10",
      "email_id": null,
      "dropped_reason": null,
      "sent_by": null
    }
  ]
}
```

- The response carries an `ETag`. Send it back as `If-Match` on an update to refuse a stale write.
- `customer.recent_emails` lists the last 10 emails you sent to the customer, only for a credential that can also read sent email (a Full access key, or `emails:read` on a token). Otherwise it is null.
- A conversation in a personal mailbox answers `404`, the same as one that does not exist.

### `PATCH /inbox/conversations/{conversation_id}`

Set the status, assign it, change labels, or file it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `If-Match` | string | The `ETag` from a read or an earlier update. A stale one is `412 precondition_failed`; `*` or none skips the check. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `open`, `pending` or `done`. |
| `assignee_id` | string \| null | A teammate who can open the channel and write, or null for nobody. |
| `labels` | object | `{ add, remove }`: team label IDs, up to 50 each. |
| `folder_id` | string \| null | A team folder to file it in, or null to take it back to the Inbox. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `snooze_until` | string \| null | A token's or session's own snooze. A snooze is a person's: an API key gets `422 snooze_needs_a_person`. Use status `pending` instead. |

Update a conversation:

```sh
curl -X PATCH "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "pending",
  "assignee_id": "0192a001-5b6c-7d8e-9f01-23456789abcd",
  "labels": {
    "add": ["0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b"]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    status: "pending",
    assignee_id: "0192a001-5b6c-7d8e-9f01-23456789abcd",
    labels: {
      add: ["0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b"]
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "status": "pending",
    "assignee_id": "0192a001-5b6c-7d8e-9f01-23456789abcd",
    "labels": {
      "add": ["0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b"]
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "inbox_conversation",
  "id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "channel": {
    "id": "0192f6a8-3c1e-7a40-9b2d-5e8f1a6c4d20",
    "address": "support@lumen.app"
  },
  "subject": "Can I move my plan to annual?",
  "status": "pending",
  "assignee": null,
  "labels": [
    {
      "id": "0192f6b2-1d4e-7f60-8a9b-0c1d2e3f4a5b",
      "name": "Billing",
      "colour": "blue"
    }
  ],
  "place": "inbox",
  "folder_id": null,
  "origin": "app",
  "external_id": "ticket_58213",
  "metadata": {
    "user_id": "usr_8412",
    "plan": "Team"
  },
  "customer": {
    "email": "priya@harborcoffee.co",
    "name": "Priya Raman",
    "contact_id": null,
    "recent_emails": null
  },
  "latest_from": "Priya Raman <priya@harborcoffee.co>",
  "snippet": "Hi! We love Lumen. Can we switch to yearly billing?",
  "message_count": 1,
  "last_message_at": "2026-10-04T09:03:12.000Z",
  "last_inbound_at": "2026-10-04T09:03:12.000Z",
  "created_at": "2026-10-04T09:03:12.000Z",
  "updated_at": "2026-10-04T09:41:55.000Z"
}
```

- Needs Inbox access `write` (Read and reply), or `inbox:write` on a token.
- Every check runs before any write, so a refused update changes nothing.
- A conversation in Spam or the Trash is `409`; archive and trash are not set through the API.

### `POST /inbox/conversations/{conversation_id}/reply`

Send a reply from the channel's own address.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | 1–256 characters, unique to this send. Replaying it inside 24 hours returns the original response instead of sending again. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `text` (required) | string | The reply. |
| `html` | string | The reply as HTML. |
| `status` | string | Set the status once the reply has gone, such as `pending`. |
| `latest_message_id` | string | The newest message you have read. If a newer one has arrived, the reply is `409 conversation_conflict` naming it. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `from` | string | The channel's address or one of its aliases. Defaults to the address the message arrived at. |
| `cc` | string[] | Visible copies. |
| `bcc` | string[] | Blind copies. |
| `subject` | string | `Re: <the original>` when absent. |
| `attachments` | object[] | `{ filename, content_type, content }`, base64 only. The same limits as `POST /emails`. |
| `reply_to_message_id` | string | The customer's message being answered; the newest one when absent. |
| `force` | boolean | Send even though a teammate is replying in Rasket Inbox right now. |

Reply to a conversation:

```sh
curl -X POST "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/reply" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-58213-1" \
  -d '{
  "text": "Hi Priya — yes, we can switch you to annual. It starts on your next bill.",
  "status": "pending"
}'
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/reply", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
    "Idempotency-Key": "reply-58213-1",
  },
  body: JSON.stringify({
    text: "Hi Priya — yes, we can switch you to annual. It starts on your next bill.",
    status: "pending"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/reply",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
        "Idempotency-Key": "reply-58213-1",
    },
    json={
    "text": "Hi Priya — yes, we can switch you to annual. It starts on your next bill.",
    "status": "pending"
  },
)

print(response.json())
```

#### Response `201`

```json
{
  "object": "inbox_reply",
  "conversation_id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "message_id": "0192f8e3-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
  "email_id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
  "from": "Lumen Support <support@lumen.app>",
  "to": ["priya@harborcoffee.co"],
  "cc": [],
  "subject": "Re: Can I move my plan to annual?",
  "status": "pending",
  "suppressed": false
}
```

- Needs Inbox access `write` (Read and reply), or `inbox:write` on a token.
- A reply is a send: it counts on your plan's sending quota like any other email, and appears on `/emails` under `email_id`.
- A teammate replying in Rasket Inbox right now is `409 conversation_conflict` unless you send `force: true`. Spam and the Trash are `409` too.
- Each key may send 60 replies an hour, on top of the team's own sending limits.

### `GET /inbox/conversations/{conversation_id}/messages`

Every message, oldest first, with its body.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | 1–20, default 10. Each message carries its body. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List a conversation's messages:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_message",
      "id": "0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5",
      "direction": "inbound",
      "from": "Priya Raman <priya@harborcoffee.co>",
      "to": ["support@lumen.app"],
      "cc": [],
      "subject": "Can I move my plan to annual?",
      "occurred_at": "2026-10-04T09:03:12.000Z",
      "text": "Hi! We love Lumen. Can we switch to yearly billing?",
      "html": null,
      "body_unavailable": false,
      "attachments": [],
      "received_email_id": "0192f8e1-7a4f-7b20-a1c3-6d5e4f3a2b10",
      "email_id": null,
      "dropped_reason": null,
      "sent_by": null
    }
  ]
}
```

- `body_unavailable: true` means the body could not be read just now, or is no longer kept. The message is still there.
- `sent_by` names the teammate or API key that sent an outbound message.

### `GET /inbox/conversations/{conversation_id}/notes`

Notes your team left. Never sent to the customer.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List a conversation's internal notes:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "inbox_note",
      "id": "0192f8e2-0b1c-7d2e-8f3a-4b5c6d7e8f90",
      "body": "Customer since 2024, on the Team plan. Offer the annual discount.",
      "author": {
        "type": "api_key",
        "api_key_id": "a4d2f0c8-5b31-4e7a-9c62-8f0b1d4e6a75",
        "name": "Lumen web app",
        "revoked": false
      },
      "created_at": "2026-10-04T09:41:55.000Z"
    }
  ]
}
```

### `POST /inbox/conversations/{conversation_id}/notes`

A note for your team, written by this key.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | 1–256 characters, unique to this send. Replaying it inside 24 hours returns the original response instead of sending again. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `body` (required) | string | Plain text, up to 10,000 characters. |
| `mentioned_user_ids` | string[] | Teammates to tell by email, from `GET /inbox/channels/{channel_id}/teammates`. Up to 10. |

Add an internal note:

```sh
curl -X POST "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "body": "Customer since 2024, on the Team plan. Offer the annual discount."
}'
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    body: "Customer since 2024, on the Team plan. Offer the annual discount."
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/notes",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "body": "Customer since 2024, on the Team plan. Offer the annual discount."
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "inbox_note",
  "id": "0192f8e2-0b1c-7d2e-8f3a-4b5c6d7e8f90",
  "body": "Customer since 2024, on the Team plan. Offer the annual discount.",
  "author": {
    "type": "api_key",
    "api_key_id": "a4d2f0c8-5b31-4e7a-9c62-8f0b1d4e6a75",
    "name": "Lumen web app",
    "revoked": false
  },
  "created_at": "2026-10-04T09:41:55.000Z"
}
```

- Needs Inbox access `write` (Read and reply), or `inbox:write` on a token. A note by a key shows the key's name, kept even after the key is revoked.

### `POST /inbox/conversations/{conversation_id}/read`

Mark it read for the person behind a token.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |

Mark a conversation read:

```sh
curl -X POST "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/read" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/read", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/read",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "inbox_read_mark",
  "conversation_id": "0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91",
  "read": false
}
```

- Read state is a person's. An API key has none, so it gets `read: false`; a token or a session marks it read for its member.

### `GET /inbox/conversations/{conversation_id}/messages/{message_id}/attachments/{attachment_id}`

A file a customer's message carried, with a link to download it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `conversation_id` (required) | string | The conversation's ID. |
| `message_id` (required) | string | The message's ID. |
| `attachment_id` (required) | string | The attachment's ID, from the message's `attachments`. |

Retrieve an attachment of a message:

```sh
curl -X GET "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages/0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5/attachments/5f2c9a1b-7e3d-4c8a-9b1f-2e6d0a4c8b73" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages/0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5/attachments/5f2c9a1b-7e3d-4c8a-9b1f-2e6d0a4c8b73", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/inbox/conversations/0192f8e1-7a40-7c55-b1e3-0f0a3b6d2e91/messages/0192f8e1-7a52-7d10-8c44-91a0b2c3d4e5/attachments/5f2c9a1b-7e3d-4c8a-9b1f-2e6d0a4c8b73",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "inbox_attachment",
  "id": "5f2c9a1b-7e3d-4c8a-9b1f-2e6d0a4c8b73",
  "filename": "damage-report.pdf",
  "content_type": "application/pdf",
  "size": 20481,
  "download_url": "https://api.rasket.com/emails/receiving/0192f8e1-7a4f-7b20-a1c3-6d5e4f3a2b10/attachments/5f2c9a1b-7e3d-4c8a-9b1f-2e6d0a4c8b73/download?expires=1791105492&token=1f0c…",
  "expires_at": "2026-10-04T09:18:12.000Z"
}
```

- `download_url` is valid for 15 minutes and takes no `Authorization` header. It is null for a part we recorded but did not keep.
- An outbound message's files are on its sent email.
