# Get messages

> GET /v1/messages · Core resources API

Source: https://developer.kustomer.com/api/core-resources/get-messages

```http
GET https://{orgname}.api.kustomerapp.com/v1/messages
Authorization: Bearer <API key>
```

## Authorization

Requires any one of these roles: `org.permission.message.read`; or legacy `org.user.message.read`. The API key or user must hold at least one of them. Legacy roles predate permission sets and still work.

Retrieves [messages](https://help.kustomer.com/message-options-H1tSk3QOI) for your Kustomer organization.

## About Messages

A message is one communication between your team and a customer, such as an email, a chat line, a text, a call or a social post, recorded inside a conversation. Creating one through the API records it on the timeline; it does not deliver anything.

Messages are the customer-visible part of a [conversation](https://developer.kustomer.com/api/core-resources#conversations). Each one belongs to a conversation and to that conversation's customer, and agents see them in order on the customer's timeline. Kustomer's native channels create messages automatically: an inbound email or chat line becomes an `in` message, and when an agent replies, the sent reply becomes an `out` message. You create messages through the API to bring in conversations from somewhere Kustomer doesn't connect to natively, to import history, or to record something your own system already sent.

#### Channel, app and direction

| Field | Meaning |
| --- | --- |
| `channel` | The kind of communication: `email`, `sms`, `chat`, `facebook`, `twitter-dm`, `twitter-tweet`, `voice`, `instagram` or `whatsapp`. Required. Responses can also contain channels you can't create, such as `instagram-comment` or `form`. |
| `app` | Which integration produced the message, such as your own `acme-inbox`. Required. Use one stable value per integration so you can recognize your own records. |
| `direction` | `in` from the customer, `out` to the customer. |
| `status` | `received`, `sent` or `error`, with details in `error` and `errorAt` when delivery failed. |

`preview` holds the text (up to 10,240 characters), `subject` the subject line for channels that have one, and `meta` any channel-specific details, such as `from` and `to` addresses. `sentAt` is when the message was sent in the source system, and it decides where the message sits on the timeline. `auto: true` marks automated messages, which are left out of reporting metrics. For email, `reference` links a reply, reply-all or forward to the message it answers. Files go in `attachments`; upload them first, as described in [Upload attachments](https://developer.kustomer.com/docs/guides/attachments).

#### Recorded, not sent

An `out` message you create through the API is a record of something already delivered. Kustomer does not send it to the customer by email, SMS or chat. To have Kustomer deliver a reply through a connected channel, create a [draft](https://developer.kustomer.com/api/core-resources#drafts) and send it. Kustomer then records the resulting `out` message for you.

Messages can't be deleted through the API. To remove sensitive content, such as a card number a customer pasted into chat, redact the message. You supply replacement `preview` text, a replacement `meta` object, or the IDs of up to 10 of the message's attachments to redact, and the replacements overwrite the original content. Redacted attachments are marked `redacted: true` and their files are no longer available.

#### Which endpoint to use

| To | Call |
| --- | --- |
| Add a message to a conversation you already track | [`POST /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/create-message-from-conversation), or [`POST /v1/messages`](https://developer.kustomer.com/api/core-resources/create-message) with `conversation` in the body |
| Add a message for a customer, creating a conversation unless you pass `conversation` | [`POST /v1/customers/{id}/messages`](https://developer.kustomer.com/api/core-resources/create-message-by-customer-id) |
| Read or update a message | [`GET`](https://developer.kustomer.com/api/core-resources/get-message-by-id), [`PUT`](https://developer.kustomer.com/api/core-resources/update-message-by-id) or [`PATCH /v1/messages/{id}`](https://developer.kustomer.com/api/core-resources/update-message-attributes-by-id) |
| Check whether you've already imported a message | [`GET /v1/messages/externalId={externalId}`](https://developer.kustomer.com/api/core-resources/get-a-message-by-external-id) |
| List a conversation's messages | [`GET /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/get-messages-by-conversation), optionally filtered by `channel` |
| List a customer's messages across conversations | [`GET /v1/customers/{id}/messages`](https://developer.kustomer.com/api/core-resources/get-messages-by-customer-id) |
| Page through all messages | [`GET /v1/messages`](https://developer.kustomer.com/api/core-resources/get-messages) |
| Remove sensitive content | [`POST /v1/messages/{id}/redactions`](https://developer.kustomer.com/api/core-resources/redact-a-message) |
| Import or update up to 100 messages per request | [`POST`](https://developer.kustomer.com/api/core-resources/bulk-create-messages) or [`PUT /v1/messages/bulk`](https://developer.kustomer.com/api/core-resources/bulk-batch-update-messages) |

A client can create 120 messages per customer per minute; set `importedAt` on historical imports to skip that limit, and keep `sentAt` as the original send time. One message can be updated 100 times in 10 minutes. Only tokens with the `org.admin` role can set `createdAt`, `createdBy`, `modifiedAt` and `modifiedBy`. Set `externalId` to the source system's message ID so retries don't create duplicates. To import history, create customers and conversations before their messages, as in [Bulk operations](https://developer.kustomer.com/docs/guides/bulk-operations).

Related: [Conversations and messages](https://developer.kustomer.com/docs/concepts/conversations-and-messages), [Bring messages from a custom channel](https://developer.kustomer.com/docs/guides/custom-channel-messages), [Drafts](https://developer.kustomer.com/api/core-resources#drafts), [Upload attachments](https://developer.kustomer.com/docs/guides/attachments)

### Recipes

#### 1. Import a message from your own channel exactly once

Put a customer's message from an in-app inbox, marketplace or partner platform on their Kustomer timeline, safely, even when your handler retries.

**Step 1.** [`GET /v1/messages/externalId={externalId}`](https://developer.kustomer.com/api/core-resources/get-a-message-by-external-id.md). Use your message ID as `externalId` and check for it first. A `200` means you've already imported this message, so stop. A `404` means it's new.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/messages/externalId=$MESSAGE_ID" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
# exits with code 4 when the message hasn't been imported yet
kusty api GET "/v1/messages/externalId=$MESSAGE_ID" --fields id
```

**Step 2.** [`POST /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/create-message-from-conversation.md). Create the message in the thread's conversation (find or create it by `externalId`; see [Conversations](https://developer.kustomer.com/api/core-resources#conversations)). `channel` and `app` are required: pick the closest native channel, such as `chat`, and one stable `app` value for your integration. `direction` is `in` for the customer's message, `preview` holds the text, and `sentAt` is when it was sent in your system, which decides its place on the timeline. Free-form details go in `meta`.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/messages" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"channel\": \"chat\", \"app\": \"acme-inbox\", \"direction\": \"in\", \"externalId\": \"$MESSAGE_ID\", \"preview\": \"$TEXT\", \"sentAt\": \"$SENT_AT\", \"meta\": {\"threadUrl\": \"$THREAD_URL\"}}"
```

```bash title="kusty"
kusty api POST /v1/conversations/$CONVERSATION_ID/messages \
  --field channel=chat --field app=acme-inbox --field direction=in \
  --field externalId=$MESSAGE_ID --field preview="$TEXT" --field sentAt=$SENT_AT \
  --field meta.threadUrl=$THREAD_URL
```

> A new inbound message on a conversation that's `done` reopens it under your org's normal rules. For backfills, set `importedAt` so the 120-per-customer-per-minute create limit doesn't apply, and use [`POST /v1/messages/bulk`](https://developer.kustomer.com/api/core-resources/bulk-create-messages). The full flow, including delivering agent replies back to your channel, is in [Bring messages from a custom channel](https://developer.kustomer.com/docs/guides/custom-channel-messages).

#### 2. Record an outbound message your system already sent

Log an order confirmation, a shipping SMS or a billing email that your own platform delivered, so agents see it in context. Kustomer doesn't send anything.

**Step 1.** [`POST /v1/customers/{id}/messages`](https://developer.kustomer.com/api/core-resources/create-message-by-customer-id.md). Create the message under the customer with `direction: "out"` and `status: "sent"`. Pass `conversation` to put it in an existing conversation; leave it out and Kustomer creates a new conversation for it. Set `auto: true` for automated notifications so they're left out of reporting metrics, and use the real send time as `sentAt`.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/customers/$CUSTOMER_ID/messages" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"channel\": \"email\", \"app\": \"acme-billing\", \"direction\": \"out\", \"status\": \"sent\", \"auto\": true, \"subject\": \"Your order has shipped\", \"preview\": \"$TEXT\", \"sentAt\": \"$SENT_AT\", \"externalId\": \"$EMAIL_ID\"}"
```

```bash title="kusty"
kusty api POST /v1/customers/$CUSTOMER_ID/messages \
  --field channel=email --field app=acme-billing --field direction=out \
  --field status=sent --field auto:=true --field subject="Your order has shipped" \
  --field preview="$TEXT" --field sentAt=$SENT_AT --field externalId=$EMAIL_ID
```

**Step 2.** [`GET /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/get-messages-by-conversation.md). The response's `relationships.conversation` holds the conversation it landed in. List that conversation's messages to check the timeline order, which follows `sentAt`.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/messages?sort=asc" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/conversations/$CONVERSATION_ID/messages --query sort=asc --all \
  | jq -r '[.data.attributes.sentAt, .data.attributes.direction, .data.attributes.preview] | @tsv'
```

> To have Kustomer actually deliver a message through a connected email, SMS or chat channel, create and send a [draft](https://developer.kustomer.com/api/core-resources#drafts) instead; Kustomer then records the `out` message for you. Creating an `out` message is only a record.

#### 3. Redact a card number a customer pasted into chat

Remove sensitive content from a message for compliance, since messages can't be deleted through the API.

**Step 1.** [`GET /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/get-messages-by-conversation.md). Find the message. Page through the conversation's messages and match `preview` against the pattern you're looking for. Keep the message `id`.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/messages?pageSize=100" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/conversations/$CONVERSATION_ID/messages --all \
  | jq -r 'select(.data.attributes.preview // "" | test("\\b(?:\\d[ -]?){13,16}\\b")) | .data.id'
```

**Step 2.** [`POST /v1/messages/{id}/redactions`](https://developer.kustomer.com/api/core-resources/redact-a-message.md). Send the replacement text as `preview`. It overwrites the original, which can't be recovered. You can instead send a replacement `meta` object, or `attachments` with the IDs of up to 10 of the message's files; redacted files are marked `redacted: true` and can no longer be downloaded.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/messages/$MESSAGE_ID/redactions" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"preview": "[card number removed]"}'
```

```bash title="kusty"
kusty api POST /v1/messages/$MESSAGE_ID/redactions --field preview="[card number removed]"
```

> Redaction needs its own permission, `org.permission.message_redaction.create`, separate from message update access.

#### 4. Export a conversation's transcript

Produce a plain-text transcript of a conversation, for example to attach to a case in another system or to archive.

**Step 1.** [`GET /v1/conversations/{id}/messages`](https://developer.kustomer.com/api/core-resources/get-messages-by-conversation.md). Page through the messages oldest first. Add `channel` to keep only one channel, such as `email`.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/conversations/$CONVERSATION_ID/messages?sort=asc&pageSize=100" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v1/conversations/$CONVERSATION_ID/messages --query sort=asc --all > messages.jsonl
```

**Step 2.** Print each message with its time and who it came from. `preview` is the text; email subjects are in `meta.subject`.

```bash title="kusty"
jq -r '.data.attributes
  | "[\(.sentAt)] \(if .direction == "in" then "Customer" else "Support" end) (\(.channel)): \(.preview // "")"' \
  messages.jsonl > transcript.txt
```

> Internal notes aren't messages. Add them from [`GET /v1/conversations/{id}/notes`](https://developer.kustomer.com/api/core-resources/get-notes-by-conversation) and merge the two lists by `createdAt` if the transcript is for your own team.

### Example object

A real record from [`GET /v1/messages/{id}`](https://developer.kustomer.com/api/core-resources/get-message-by-id.md):

```json
{
  "type": "message",
  "id": "57c8d2b01a57ed1400392073",
  "attributes": {
    "externalId": "<CADBFQxRPMLtQrBS9x+mt=7Wcy0GEw0FxcneO5sn=k4XgGDyVZg@mail.gmail.com>",
    "channel": "email",
    "app": "postmark",
    "size": 0,
    "direction": "in",
    "preview": "Can you please help me?",
    "sentiment": {
      "confidence": 1,
      "polarity": 1
    },
    "meta": {
      "subject": "Re: hey",
      "from": "jeremy@kustomer.com",
      "to": [
        {
          "email": "support@kustomer.com"
        }
      ],
      "cc": [],
      "recipient": {
        "email": "support@kustomer.com",
        "mailboxHash": ""
      },
      "inReplyTo": "<ef265bbbd801e5e38722f66fa3819c79cd07dc7971b4ad84fafce7e57737e911@kustomer.mail.kustomerapp.com>"
    },
    "status": "received",
    "lang": "zh_cn",
    "sentAt": "2016-09-02T01:15:14.000Z",
    "createdAt": "2016-09-02T01:15:31.140Z",
    "updatedAt": "2016-11-13T16:34:12.214Z",
    "modifiedAt": "2016-11-13T16:34:12.214Z"
  },
  "relationships": {
    "org": {
      "links": {
        "self": "/v1/orgs/57bb5d2e32e83a130052e94e"
      },
      "data": {
        "type": "org",
        "id": "57bb5d2e32e83a130052e94e"
      }
    },
    "modifiedBy": {
      "links": {
        "self": "/v1/users/57bb5d2e32e83a130052e94f"
      },
      "data": {
        "type": "user",
        "id": "57bb5d2e32e83a130052e94f"
      }
    },
    "customer": {
      "links": {
        "self": "/v1/customers/57c08ccde595371400462df2"
      },
      "data": {
        "type": "customers",
        "id": "57c08ccde595371400462df2"
      }
    },
    "conversation": {
      "links": {
        "self": "/v1/conversations/57c8d2b365f7cc14004e674f"
      },
      "data": {
        "type": "conversation",
        "id": "57c8d2b365f7cc14004e674f"
      }
    }
  },
  "links": {
    "self": "/v1/messages/57c8d2b01a57ed1400392073",
    "related": "/v1/postmark/messages/57c8d2b01a57ed1400392073"
  }
}
```

Fields:

- `attributes.externalId` (string): The message's ID in the system it came from, up to 256 characters. For email it's the email's `Message-ID` header, as here. Set it on messages you create so `GET /v1/messages/externalId={externalId}` can tell you whether you already imported one.
- `attributes.channel` (string): The kind of communication. You can create `email`, `sms`, `chat`, `facebook`, `twitter-dm`, `twitter-tweet`, `voice`, `instagram` or `whatsapp` messages; responses can also show other channels, such as `instagram-comment` or `form`.
- `attributes.app` (string): Which integration produced the message. Kustomer's own channels set it, as `postmark` does here for email; on messages you create it's required, and you should use one stable value per integration, such as `acme-inbox`.
- `attributes.direction` (string): `in` from the customer, `out` from your team to the customer.
- `attributes.preview` (string): The message text, up to 10,240 characters. Redaction replaces it.
- `attributes.sentiment` (object): Sentiment of this message. `polarity` is `-1`, `0` or `1`; `confidence` runs from `-1` to `1`.
- `attributes.meta` (object): Channel-specific details. For email that's the `subject`, `from`, `to`, `cc`, the `recipient` mailbox and `inReplyTo`; on messages you create it's free-form.
- `attributes.meta.recipient` (object): The support address the email was delivered to.
- `attributes.meta.inReplyTo` (string): The `Message-ID` of the email this one replies to.
- `attributes.status` (string): `received` for inbound messages, `sent` for delivered outbound ones, or `error` when delivery failed, with details in `error` and `errorAt`.
- `attributes.lang` (string): The language of the message, as a language code. Writable, up to 100 characters.
- `attributes.sentAt` (string): When the message was sent in its source system. It decides where the message sits on the timeline, so keep the original time when you import history.
- `attributes.createdAt` (string): When Kustomer recorded the message, which can be later than `sentAt`. Only tokens with the `org.admin` role can set it.
- `relationships.customer` (object): The customer the message is with, always the same as the conversation's customer.
- `relationships.conversation` (object): The conversation the message belongs to.

### Relationships

- Points to **Customers** via `relationships.customer`: The customer a message was exchanged with. It's always the customer of the message's conversation, and a customer's messages can be listed across all their conversations.
- Points to **Conversations** via `relationships.conversation`: The conversation a message belongs to. Every message is in exactly one conversation, which can hold many messages.
- Referenced by **Attachments** via `relationships.message`: The message a file is attached to. A message can carry several attachments, and its files are read through `GET /v1/messages/{message_id}/attachments/{id}`.
- Referenced by **Conversations** via `relationships.messages` (list): The messages in a conversation, in both directions. Each message belongs to exactly one conversation.
- Referenced by **Customers** via `relationships.messages` (list): Every message exchanged with this customer, across all of their conversations. Each message also points back to its customer.

### Permissions

Fewest roles for a read-only integration: `org.permission.message.read`. For every endpoint: `org.permission.message.read`, `org.permission.message.create`, `org.permission.message.update`, `org.permission.message_redaction.create`.

### Events

Webhook events:

- `kustomer.message.create`: A message is sent or received on any channel
- `kustomer.message.update`: A message changes, for example its delivery `status`

Workflow triggers:

- `kustomer.message.create` (Message Created): An inbound or outbound message is created
- `kustomer.message.update` (Message Updated): A message changes, for example its delivery status

Workflow actions:

- `kustomer.message.find-last` (Customer: Lookup Last Message): required inputs `customer`
- `kustomer.message.create` (Message: Create): required inputs `customer`, `channel`, `app`, `size`
- `kustomer.message.update` (Message: Update): required inputs `id`
- `kustomer.message.find-by-id` (Message: Lookup (By ID)): required inputs `id`
- `kustomer.message.find-by-external` (Message: Lookup (By External ID)): required inputs `identifier`

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number of results to return. |
| `pageSize` | integer | no | Number of results to return per page. |
| `sort` | string | no | Sort direction for results. |
| `include` | string | no | List of resources to include in this response. Use a comma to separate resources. Supports the `attachments` resource. |

## Responses

### 200

OK

- `meta` (object, required)
  - `pageSize` (integer, required)
  - `page` (integer, required)
- `data` (array<object>, required)
  - `type` (string, required)
  - `id` (string, required)
  - `attributes` (object, required)
    - `externalId` (string, required)
    - `channel` (string, required)
    - `app` (string, required)
    - `size` (integer, required)
    - `direction` (string, required)
    - `preview` (string, required)
    - `sentiment` (object, required)
    - `meta` (object, required)
    - `status` (string, required)
    - `sentAt` (string, required)
    - `createdAt` (string, required)
    - `updatedAt` (string, required)
  - `relationships` (object, required)
    - `org` (object, required)
    - `customer` (object, required)
    - `conversation` (object, required)
  - `links` (object, required)
    - `self` (string, required)
    - `related` (string, required)
- `links` (object, required)
  - `self` (string, required)
  - `first` (string, required)
  - `prev` (string, required, nullable)
  - `next` (string, required, nullable)

```json
{
  "meta": {
    "pageSize": 100,
    "page": 1
  },
  "data": [
    {
      "type": "message",
      "id": "57c8d2b01a57ed1400392073",
      "attributes": {
        "externalId": "<CADBFQxRPMLtQrBS9x+mt=7Wcy0GEw0FxcneO5sn=k4XgGDyVZg@mail.gmail.com>",
        "channel": "email",
        "app": "postmark",
        "size": 0,
        "direction": "in",
        "preview": "Can you please help me?",
        "sentiment": {},
        "meta": {
          "inReplyTo": "<ef265bbbd801e5e38722f66fa3819c79cd07dc7971b4ad84fafce7e57737e911@kustomer.mail.kustomerapp.com>",
          "recipient": {
            "mailboxHash": "",
            "email": "support@kustomer.com"
          },
          "cc": [],
          "to": [
            {
              "email": "support@kustomer.com"
            }
          ],
          "from": "jeremy@kustomer.com",
          "subject": "Re: hey"
        },
        "status": "received",
        "sentAt": "2016-09-02T01:15:14.000Z",
        "createdAt": "2016-09-02T01:15:31.140Z",
        "updatedAt": "2016-09-02T01:15:31.140Z"
      },
      "relationships": {
        "org": {
          "links": {
            "self": "/v1/orgs/57bb5d2e32e83a130052e94e"
          },
          "data": {
            "type": "org",
            "id": "57bb5d2e32e83a130052e94e"
          }
        },
        "customer": {
          "links": {
            "self": "/v1/customers/57c08ccde595371400462df2"
          },
          "data": {
            "type": "customers",
            "id": "57c08ccde595371400462df2"
          }
        },
        "conversation": {
          "links": {
            "self": "/v1/conversations/57c8d2b365f7cc14004e674f"
          },
          "data": {
            "type": "conversation",
            "id": "57c8d2b365f7cc14004e674f"
          }
        }
      },
      "links": {
        "self": "/v1/messages/57c8d2b01a57ed1400392073",
        "related": "/v1/postmark/messages/57c8d2b01a57ed1400392073"
      }
    }
  ],
  "links": {
    "self": "/v1/messages?page=1&pageSize=100",
    "first": "/v1/messages?page=1&pageSize=100",
    "prev": null,
    "next": null
  }
}
```

### 401

Unauthorized

### 403

Forbidden

## Example request

```bash
curl --request GET \
  --url 'https://api.kustomerapp.com/v1/messages' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```
