# CRM · Inbox

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /crm/inbox/conversations

**List inbox conversations**

`operationId: CRMController_inboxDistinctConversations`

Groups the caller's messages into conversations and returns one entry per thread — the list view of an inbox.

`groupBy` decides what counts as a conversation. The default, `contact_channel`, treats one contact on one channel as a thread, so the same person's email and SMS appear separately.

Covers `email`, `sms` and `chat` only. Social DMs are in `social_activity` and do not appear here.

#### Signature

```http
GET /crm/inbox/conversations (groupBy?: string) -> One entry per conversation
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The `parties` value on each conversation is what the thread endpoints take.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `GET /crm/inbox/conversations/{parties}`
- `GET /crm/inbox/messages`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `groupBy` | query | string | — | How messages are grouped into threads. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | One entry per conversation |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/inbox/conversations/{parties}

**Get messages in a conversation**

`operationId: CRMController_inboxGetConversationMessages`

Every message in one thread, in order. Narrow with `channel` when a contact has been reached on more than one.

#### Signature

```http
GET /crm/inbox/conversations/{parties} (parties: string, channel?: string) -> The thread's messages
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `DELETE /crm/inbox/thread/{parties}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `parties` | path | string | yes | Conversation key identifying the participants, as returned by `GET /crm/inbox/conversations`. |
| `channel` | query | string | — | Restrict to one channel — `email`, `sms`, `chat`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The thread's messages |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/inbox/messages

**List inbox messages**

`operationId: CRMController_inboxGetMessages`

A flat, paged list of the caller's messages, newest first by default. Use the conversation endpoints when you want them threaded.

#### Signature

```http
GET /crm/inbox/messages (p?: integer, ps?: integer, l?: string, s?: string, st?: string, en?: boolean) -> A page of messages
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `GET /crm/inbox/messages/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `l` | query | string | — | Cursor for keyset pagination: the sort value of the last record from the previous page. Pass it to continue past that record instead of skipping with `p`, which stays fast at any depth. |
| `en` | query | boolean | — | Resolve linked records inline instead of returning bare references. |
| `p` | query | integer | — | Page number, 1-based. |
| `ps` | query | integer | — | Page size — how many records to return. Large values are slower; prefer cursor paging via `l` for deep scans. |
| `s` | query | string | — | Field to sort by. |
| `st` | query | "asc" \| "desc" | — | Sort direction. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of messages |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/inbox/messages/{id}

**Get an inbox message**

`operationId: CRMController_inboxGetMessage`

Fetches one message. Pass `en=true` to resolve its linked records — the contact, the related order — inline.

#### Signature

```http
GET /crm/inbox/messages/{id} (id: string, en?: boolean) -> The message
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |
| `404` | MESSAGE_NOT_FOUND | message not found | No message has that id. | Check the id with `GET /crm/inbox/messages`. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `POST /crm/inbox/update-status/{messageId}/{status}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Message id. |
| `en` | query | boolean | — | Resolve linked records inline instead of returning bare references. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The message |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | message not found — No message has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /crm/inbox/delete/{id}

**Delete an inbox message**

`operationId: CRMController_inboxDeleteMessage`

Deletes one message belonging to the caller. Deleting a whole thread is a separate endpoint.

#### Signature

```http
DELETE /crm/inbox/delete/{id} (id: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |
| `400` | CUSTOMER_MISMATCH | customer does not match | The record belongs to a different customer than the caller. | You can only act on your own messages. |
| `404` | MESSAGE_NOT_FOUND | message not found | No message has that id. | Check the id with `GET /crm/inbox/messages`. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `DELETE /crm/inbox/thread/{parties}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Message id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `400` | customer does not match — The record belongs to a different customer than the caller. |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | message not found — No message has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /crm/inbox/thread/{parties}

**Delete a conversation**

`operationId: CRMController_inboxDeleteThread`

Deletes every message in a thread. Pass `channel` to remove only one channel's messages, leaving the contact's other conversations intact.

This removes the whole history with that contact — there is no undo.

#### Signature

```http
DELETE /crm/inbox/thread/{parties} (parties: string, channel?: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Omitting `channel` deletes the thread across every channel with that contact.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `DELETE /crm/inbox/delete/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `channel` | query | string | — | Restrict to one channel — `email`, `sms`, `chat`. |
| `parties` | path | string | yes | Conversation key identifying the participants, as returned by `GET /crm/inbox/conversations`. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/inbox/update

**Update or send a message**

`operationId: CRMController_inboxUpdateMessage`

Saves a message, and **sends it when `send=true`** — the same endpoint covers saving a draft and dispatching it.

A duplicate guard rejects an identical payload posted twice with a `409`, so a double-clicked send does not mail the customer twice. The error says plainly what happened rather than surfacing as a generic failure.

#### Signature

```http
POST /crm/inbox/update (send?: boolean, body) -> The saved message
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Sending is driven by the `send` query parameter, not the body — it is easy to save when you meant to send.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |
| `409` | DUPLICATE_MESSAGE | Message was not saved — an identical message already exists. Change the message, or open the existing one to send it. | An identical payload has already been saved — typically a double-clicked send. | Treat this as success for a retry. The message already exists; open it rather than resending. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `POST /crm/send-template`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `send` | query | boolean | — | `true` dispatches the message as well as saving it. |

### Request body

The message to save or send.

```json
{
  "data": {
    "channel": "email",
    "to": "ada@example.com",
    "subject": "Your order",
    "body": "Hello…"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The saved message |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `409` | Message was not saved — an identical message already exists. Change the message, or open the existing one to send it. — An identical payload has already been saved — typically a double-clicked send. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/inbox/update-status/{messageId}/{status}

**Set a message status**

`operationId: CRMController_inboxUpdateMessageStatus`

Changes a message's status — marking it read, archived or handled. Both values are path segments rather than a body.

#### Signature

```http
POST /crm/inbox/update-status/{messageId}/{status} (messageId: string, status: string) -> The updated message
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |
| `400` | CUSTOMER_MISMATCH | customer does not match | The record belongs to a different customer than the caller. | You can only act on your own messages. |
| `404` | MESSAGE_NOT_FOUND | message not found | No message has that id. | Check the id with `GET /crm/inbox/messages`. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `GET /crm/inbox/messages/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `messageId` | path | string | yes | Message id. |
| `status` | path | string | yes | New status, e.g. `read`, `unread`, `archived`. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated message |
| `400` | customer does not match — The record belongs to a different customer than the caller. |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | message not found — No message has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/inbox/notifications/{id}

**Get inbox notifications**

`operationId: CRMController_inboxGetNotifications`

The caller's notifications. Supply `id` for one; omit the segment to list them all.

#### Signature

```http
GET /crm/inbox/notifications/{id} (id: string) -> Notifications
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `POST /crm/inbox/save-push-token`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Notification id. Omit to list all. |
| `en` | query | boolean | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Notifications |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/inbox/save-push-token

**Save a push notification token**

`operationId: CRMController_inboxSaveToken`

Registers a device token so the caller can receive push notifications. Call it on every app launch — tokens are rotated by the platforms and a stale one silently stops delivering.

#### Signature

```http
POST /crm/inbox/save-push-token (body) -> The saved token record
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `401` | CUSTOMER_REQUIRED | customer is required | No signed-in customer or user could be resolved. | Sign in — these routes are scoped to the caller. |

Plus the standard platform errors: `403`, `429`, `500`.

#### See also

- `GET /crm/inbox/notifications/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The device token.

```json
{
  "token": "fcm_dGhpcyBpcyBhIHRva2Vu",
  "platform": "ios"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The saved token record |
| `401` | customer is required — No signed-in customer or user could be resolved. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

