# Bring messages from a custom channel

> Record conversations from your own messaging channel (an in-app inbox, a marketplace, a partner platform) on Kustomer timelines, thread them correctly, and deliver agent replies back.

Source: https://developer.kustomer.com/docs/guides/custom-channel-messages

Kustomer has native email, chat, SMS, voice and social channels. If your customers also talk to you somewhere else, such as an in-app inbox, a marketplace's messaging system or a partner platform, you can bring those conversations into Kustomer so agents see everything on one timeline.

This guide covers the inbound path (customer messages into Kustomer), threading, and the outbound path (agent replies back to your channel).

```mermaid
sequenceDiagram
  autonumber
  participant Ch as Your channel
  participant S as Your integration
  participant K as Kustomer API
  participant A as Agent
  Ch->>S: New customer message
  S->>K: Find or create customer (externalId)
  S->>K: Find conversation for thread (externalId) or create one
  S->>K: POST message (direction: in)
  A->>K: Agent replies in Kustomer
  K-->>S: Webhook kustomer.message.create (direction: out)
  S->>Ch: Deliver reply
```

## How a custom channel maps to Kustomer

| Your channel     | Kustomer            | Key field                                                                                              |
| ---------------- | ------------------- | ------------------------------------------------------------------------------------------------------ |
| User account     | Customer            | `externalId` = your user ID                                                                            |
| Thread           | Conversation        | `externalId` = your thread ID                                                                          |
| Message          | Message             | `externalId` = your message ID                                                                         |
| Your integration | The message's `app` | A stable identifier, such as `acme-inbox`                                                              |
| Message type     | `channel`           | The closest native channel: `chat` for real-time messaging, `sms` for text-like, `email` for long-form |

Messages created through the API are records: Kustomer doesn't deliver them anywhere. Your integration is responsible for delivering agent replies to your channel.

## 1. Find or create the customer

```ts title="Node.js"
async function customerFor(user: { id: string; name: string; email?: string }) {
  const found = await kfetch(`/v1/customers/externalId=${encodeURIComponent(user.id)}`)
  if (found.ok) return (await found.json()).data.id
  const created = await kfetch('/v1/customers', {
    method: 'POST',
    body: JSON.stringify({ externalId: user.id, name: user.name, emails: user.email ? [{ type: 'home', email: user.email }] : [] }),
  }).then((r) => r.json())
  return created.data.id
}
```

## 2. Find or create the conversation

Use your thread ID as the conversation's `externalId`, so every message in the thread lands in the same conversation:

`GET /v1/conversations/externalId={externalId}` ([reference](https://developer.kustomer.com/api/core-resources/get-conversation-by-external-id))

`POST /v1/customers/{id}/conversations` ([reference](https://developer.kustomer.com/api/core-resources/create-conversation-for-customer))

```ts title="Node.js"
async function conversationFor(customerId: string, thread: { id: string; subject: string }) {
  const found = await kfetch(`/v1/conversations/externalId=${encodeURIComponent(thread.id)}`)
  if (found.ok) return (await found.json()).data.id
  const created = await kfetch(`/v1/customers/${customerId}/conversations`, {
    method: 'POST',
    body: JSON.stringify({ externalId: thread.id, name: thread.subject, direction: 'in', tags: ['acme-inbox'] }),
  }).then((r) => r.json())
  return created.data.id
}
```

Tag or set a custom attribute on these conversations (for example `custom.sourceChannelStr: "acme-inbox"`) so routing rules, reports and your webhook handler can recognize them.

## 3. Create the inbound message

`POST /v1/conversations/{id}/messages` ([reference](https://developer.kustomer.com/api/core-resources/create-message-from-conversation))

```json title="Request body"
{
  "channel": "chat",
  "app": "acme-inbox",
  "direction": "in",
  "preview": "Hi, the seller hasn't shipped my order yet. Can you help?",
  "externalId": "msg_01HF3Q8Y2V",
  "sentAt": "2026-10-21T14:02:11Z",
  "meta": { "threadUrl": "https://inbox.acme.example/t/88123" }
}
```

- `preview` holds the message text, up to 10,240 characters.
- `externalId` lets you skip messages you've already imported: check [get message by external ID](https://developer.kustomer.com/api/core-resources/get-a-message-by-external-id) before creating.
- `meta` is free-form. Put channel-specific details there.
- Attachments: upload them first, then pass them in `attachments`. See [Upload attachments](https://developer.kustomer.com/docs/guides/attachments).

When a new inbound message arrives on a conversation that's `done`, Kustomer's normal reopen and routing rules apply.

## 4. Deliver agent replies

Subscribe an [outbound webhook](https://developer.kustomer.com/docs/events/outbound-webhooks) to `kustomer.message.create`, then deliver outbound messages that belong to your channel:

```ts title="Node.js"
app.post('/kustomer/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifySignature(req)) return res.sendStatus(401)
  const event = JSON.parse(req.body.toString())
  res.sendStatus(200)

  if (event.name !== 'kustomer.message.create') return
  const msg = event.data
  if (msg.attributes.direction !== 'out' || msg.attributes.app === 'acme-inbox') return // ignore inbound and our own records

  const conversation = await kfetch(`/v1/conversations/${msg.relationships.conversation.data.id}`).then((r) => r.json())
  if (!conversation.data.attributes.tags?.includes('acme-inbox')) return // not our channel

  await acmeInbox.send(conversation.data.attributes.externalId, msg.attributes.preview)
})
```

> **Test how agents reply in your org:** Which reply channel agents get on these conversations depends on your org's channel setup. Test the full loop in a sandbox org: create an inbound message, reply as an agent, and confirm the `kustomer.message.create` event your handler receives. Adjust the filter in your handler to match.

## 5. Import history

To backfill old threads, use [bulk message creation](https://developer.kustomer.com/api/core-resources/bulk-create-messages) and set `importedAt` on each message. Imported records skip the per-customer create limit (120 per minute), and `sentAt` keeps the original timeline order. Create customers and conversations first, then their messages.

## Checklist

- [ ] Customer, conversation and message all have `externalId` set.
- [ ] Every message uses the same `app` value.
- [ ] Inbound handler is idempotent (it checks `externalId` before creating).
- [ ] Webhook handler verifies signatures, acknowledges within a few seconds and ignores its own records.
- [ ] Historical imports set `importedAt`.
