Developer Guide · Guides

Widget identity & merge

How Inboxi turns website and app chat sessions into contacts: what a visitorId is, what identify() stores, how chats from the same person are grouped, and how to model one contact with many support tickets.

When someone chats through your website widget or mobile SDK, Inboxi has to decide two things: which conversation (ticket) the message belongs to, and which person (contact) that conversation belongs to. Inboxi keeps these separate on purpose: conversations are matched by session, contacts are grouped by identity. The SDK calls themselves are documented in the SDK section.

The visitorId: one conversation

Each SDK session starts with POST /api/widget/sdk/init. The userId you pass there becomes the session'svisitorId (Inboxi generates one if you don't). A conversation is stored per visitor:

  1. Same visitorId (and same widget): the existing conversation is reused. Closing and reopening the app continues the same thread.
  2. New visitorId: a new conversation is created. Inboxi deliberately does not merge it into an older one, because that would collapse separate support threads into one.
init(userId)
visitorId for this session
opens
Conversation
Matched by visitorId only
enriched by
identify()
Adds email, phone, userId, data
grouped into
Contact
Grouped by identity keys

identify(): who the person is

identify() (POST /api/widget/sdk/identify) attaches identity to the session's conversation. name is required; everything else is optional but strongly recommended.

FieldStored asUsed for
nameContact nameChat header, contact card. Keep it clean: the person's name only.
emailmetadata.emailGrouping chats into one contact; contact panel.
phonemetadata.realPhoneGrouping; order lookups by the AI agent; contact panel.
userIdmetadata.externalUserIdYour user ID. Echoed back in every webhook's contact block; filter with ?externalUserId=.
datametadata.customDataAny extra fields (plan, ticket number, city). Shown as custom fields; filter with ?customData.plan=pro.
javascript
Inboxi.identify({
  userId: 'u_12345',
  name:   'Ahmed Ali',
  email:  'ahmed@example.com',
  phone:  '201001234567',
  data:   { plan: 'pro', city: 'Riyadh' }
});

Over time each conversation accumulates these identity keys in its metadata:

json
{
  "email": "ahmed@example.com",
  "realPhone": "201001234567",
  "externalUserId": "u_12345",
  "customData": { "plan": "pro", "city": "Riyadh" },
  "knownVisitorIds": ["3f1c…-a91b_t42", "3f1c…-a91b"],
  "baseVisitorId": "3f1c…-a91b",
  "lastTicketId": "12345",
  "lastSubject": "Booking issue",
  "ticketHistory": [{ "id": "12345", "subject": "Booking issue", "at": "2026-10-10T09:00:00Z" }]
}

How contacts are merged

On the Contacts page, conversations that share an email, a realPhone, a baseVisitorId or any of theknownVisitorIds are shown as one contact with a “N chats” badge. Open the contact to see every individual conversation in its Chats tab. The inbox still shows each conversation separately, so agents work ticket by ticket while seeing the whole history of the person.

  • knownVisitorIds collects every visitorId that has been used for a conversation.
  • baseVisitorId is the device ID with any per-ticket suffix removed. Suffixes like _t535703, _ticket_1776… or _session_… are stripped, so tickets from the same device are recognised as the same person even without an email or phone.

Pattern: one contact, many tickets

If your app opens a new support ticket each time, you want separate threads in the inbox but one contact per customer. Do both things:

  1. 1
    Unique visitorId per ticket
    Start the session with a userId such as `${user.id}_ticket_${ticket.id}`. Each ticket becomes its own conversation.
  2. 2
    Stable identity in identify()
    Pass the same email/phone every time, and put ticket details in data (ticket_id, ticket_subject), not in the name.
  3. 3
    Result
    Agents see one conversation per ticket; the Contacts page shows one person with all tickets; webhooks carry your IDs in the contact block.
javascript
// User opens ticket #369841
Inboxi.identify({
  userId: `clinic_${user.clinicId}_ticket_${ticket.id}`,  // unique per ticket
  name:   user.clinicName,                                   // just the name
  email:  user.email,
  phone:  user.phone,
  data:   { ticket_id: ticket.id, ticket_subject: ticket.subject, clinic_id: user.clinicId }
});

For “one conversation per customer” instead, simply use the same userId every time.

Composite name parser

Some integrations put several values in the name, like "Ahmed (Clinic) | #12345 | Booking issue". Inboxi recognises the | separator and splits it: the first part becomes the contact name, a #number part becomes metadata.lastTicketId, the next part becomes metadata.lastSubject, and each ticket is added tometadata.ticketHistory (last 20 kept). Agents see only the clean name in the chat list and header.

This is a safety net, not a recommendation. Send a clean name and put ticket details in data so they appear as structured, searchable custom fields.

Reading conversations for a visitor

GET /api/v1/conversations accepts visitorId (matches the session and merged knownVisitorIds),externalUserId, email and customData.<key> filters. If you call it from an embedded context with a widget-scoped key, always pass visitorId so a visitor only ever sees their own chats. Details in the conversations reference.