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.

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).

AgentKustomer APIYour integrationYour channelAgentKustomer APIYour integrationYour channelNew customer message1Find or create customer (externalId)2Find conversation for thread (externalId) or create one3POST message (direction: in)4Agent replies in Kustomer5Webhook kustomer.message.create (direction: out)6Deliver reply7
Diagram source
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 channelKustomerKey field
User accountCustomerexternalId = your user ID
ThreadConversationexternalId = your thread ID
MessageMessageexternalId = your message ID
Your integrationThe message's appA stable identifier, such as acme-inbox
Message typechannelThe 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

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} post/v1/customers/{id}/conversations
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
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 before creating.
  • meta is free-form. Put channel-specific details there.
  • Attachments: upload them first, then pass them in attachments. See Upload 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 to kustomer.message.create, then deliver outbound messages that belong to your channel:

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)
})

5. Import history

To backfill old threads, use bulk message creation 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.