PlatformXeDocs
Get API Key

Contextual Messaging — Tenant Onboarding Guide

Step-by-step guide for tenants to set up entity-scoped conversations in their application.

This guide walks you through integrating PlatformXe Contextual Messaging into your application. By the end, your users will be able to have multi-party conversations scoped to your business entities — bookings, orders, appointments, claims, or whatever your domain requires.

Prerequisites

  • A PlatformXe organization with an active plan
  • An API key with threads:admin, threads:read, and threads:write scopes
  • Your application's backend can make authenticated HTTP calls to PlatformXe

Step 1: Define Your Channels

A channel represents an entity type in your domain. Think about what your users have conversations about:

Your DomainEntity TypeExample Channel SlugParticipant Roles
Property managementLeaseleaseTENANT, LANDLORD, MANAGER
HealthcareAppointmentappointmentPATIENT, PROVIDER, SPECIALIST
InsuranceClaimclaimCLAIMANT, ADJUSTER, BROKER
LogisticsShipmentshipmentSHIPPER, CARRIER, BROKER
HospitalityBookingbookingGUEST, HOST, SUPPORT
Services marketplaceOrderservice-orderCUSTOMER, PROVIDER, SUPPORT

Create each channel:

curl -X POST https://api.platformxe.com/api/v1/threads/channels \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "booking",
    "displayName": "Booking Conversations",
    "entityType": "BOOKING",
    "participantRoles": ["GUEST", "HOST", "SUPPORT"],
    "defaultVisibility": ["ALL"],
    "lifecycleRules": {
      "autoClose": {
        "onEntityStatus": ["COMPLETED", "CANCELLED"]
      },
      "autoArchive": {
        "afterClosedDays": 90
      },
      "systemMessages": {
        "onThreadCreated": "A new conversation has been started.",
        "onThreadClosed": "This conversation has been closed ({closedReason})."
      }
    }
  }'

Key decisions:

  • Participant roles are strings you define. They mean nothing to PlatformXe — they're labels your application uses to identify who's who.
  • Default visibility controls who sees messages when the sender doesn't specify. ["ALL"] means everyone by default.
  • Lifecycle rules automate thread closure when your entity changes state.

Step 2: Create Threads When Entities Are Created

When a business entity is created in your application (e.g., a booking is confirmed), create a thread:

// Your backend — on entity creation
const thread = await platformx.threads.createThread({
  channelSlug: 'booking',
  entityId: booking.id,           // Your entity's ID
  subject: `Booking ${booking.reference}`,
  metadata: {                     // Optional — store context
    propertyName: booking.property.name,
    checkInDate: booking.checkInDate,
  },
  participants: [
    {
      role: 'GUEST',
      externalId: booking.guestId,       // User ID in YOUR system
      displayName: booking.guestName,
    },
    {
      role: 'HOST',
      externalId: booking.hostId,
      displayName: booking.hostName,
    },
    {
      role: 'SUPPORT',
      externalId: 'system',
      displayName: 'Support',
    },
  ],
});

Important: externalId is the user's ID in your application, not in PlatformXe. PlatformXe never authenticates your end users — your application is the authority on identity.

Step 3: Build Your Proxy API

Your frontend should never call PlatformXe directly. Build thin proxy routes in your backend that:

  1. Authenticate the user using your own auth system
  2. Determine their role (GUEST, HOST, etc.) from the session
  3. Resolve the thread by entity ID
  4. Proxy to PlatformXe with the correct participant context
Frontend                  Your Backend               PlatformXe
   |                          |                          |
   |-- GET /conversations --->|                          |
   |   (session cookie)       |-- resolve user role ---> |
   |                          |-- GET /threads?entity -->|
   |                          |   (x-api-key)            |
   |                          |<-- thread + messages ----|
   |<-- filtered messages ----|                          |

Example proxy route:

// GET /api/conversations/:entityId/messages
export async function GET(request, { params }) {
  const user = await authenticateUser(request);  // Your auth
  const role = resolveRole(user);                 // Your logic

  // Find thread by entity ID
  const threads = await platformx.threads.listThreads({
    channelSlug: 'booking',
    entityId: params.entityId,
  });

  if (!threads.length) {
    return json({ messages: [] });
  }

  // List messages — PlatformXe filters by role visibility
  const messages = await platformx.threads.listMessages(
    threads[0].id,
    { role }   // Pass as X-Participant-Role header
  );

  return json({ messages });
}

Step 4: Send Messages

When a user sends a message, your proxy identifies them and forwards to PlatformXe:

// POST /api/conversations/:entityId/messages
export async function POST(request, { params }) {
  const user = await authenticateUser(request);
  const role = resolveRole(user);
  const { content, visibility } = await request.json();

  const thread = await findOrCreateThread(params.entityId, user, role);

  const message = await platformx.threads.sendMessage(thread.id, {
    senderExternalId: user.id,
    senderRole: role,
    content,
    visibility: visibility || ['ALL'],
  });

  return json({ message });
}

Visibility Control

Let users choose who sees their message:

VisibilityEffect
["ALL"]Every participant sees it
["HOST", "SUPPORT"]Only host and support — guest excluded
["GUEST", "SUPPORT"]Only guest and support — host excluded
["SUPPORT"]Internal note — only your support team

The sender's role is always implicitly included.

Step 5: Forward Entity Events for Lifecycle

When your entity changes state, tell PlatformXe so it can auto-close threads:

// When a booking completes
await platformx.threads.entityEvent({
  channelSlug: 'booking',
  entityId: booking.id,
  event: 'STATUS_CHANGED',
  newStatus: 'COMPLETED',    // Must match autoClose.onEntityStatus
});

If the channel's lifecycle rules include COMPLETED in autoClose.onEntityStatus, the thread closes automatically and a system message is posted.

Step 6: Build the UI

The UI is yours to build — PlatformXe provides the data, you provide the experience. Common patterns:

Embedded in Entity Detail

Show the conversation inside the entity detail page (e.g., booking detail, order detail). This is the most natural pattern — users see the conversation in the context of what they're discussing.

Inbox / Conversation List

Use the inbox endpoint to show all conversations for a user:

const inbox = await platformx.threads.inbox({
  externalId: user.id,
  role: 'GUEST',
  status: 'OPEN',
});

// Each item has: thread, channel, unreadCount, lastMessage

Unread Badge

Show unread count in your navigation:

const { count } = await platformx.threads.unreadCount({
  externalId: user.id,
  role: 'GUEST',
});

Step 7: Handle Webhooks (Optional)

Subscribe to thread events for real-time reactions:

await platformx.webhooks.create({
  name: 'Thread notifications',
  url: 'https://your-app.com/webhooks/threads',
  events: ['message.created', 'thread.closed'],
});

When a new message arrives, you can:

  • Send a push notification to the recipient
  • Send an email summary
  • Update unread counts in your cache
  • Trigger your own workflows

Complete Integration Checklist

  • Create channels for each entity type
  • Create threads on entity creation (with all participants)
  • Build proxy routes (authenticate, resolve role, proxy to PlatformXe)
  • Build conversation UI (message list, compose bar, visibility controls)
  • Forward entity status changes for lifecycle auto-close
  • Add inbox/unread count to navigation
  • Subscribe to webhook events for notifications (optional)
  • Mark messages as read when user views the conversation

SDK Quick Reference

import { PlatformXeClient } from '@caldera/platformxe-sdk';

const client = new PlatformXeClient({
  apiKey: process.env.PLATFORMXE_API_KEY,
});

// Channels
client.threads.createChannel({ ... })
client.threads.listChannels()

// Threads
client.threads.createThread({ channelSlug, entityId, participants })
client.threads.listThreads({ channelSlug, entityId, status })
client.threads.closeThread(threadId, { reason })

// Messages
client.threads.sendMessage(threadId, { senderExternalId, senderRole, content, visibility })
client.threads.listMessages(threadId, { role })
client.threads.sendSystemMessage(threadId, { content })

// Read State
client.threads.markRead(threadId, { participantExternalId, participantRole })
client.threads.inbox({ externalId, role })
client.threads.unreadCount({ externalId, role })

// Lifecycle
client.threads.entityEvent({ channelSlug, entityId, event, newStatus })