adventist-inbox-sdk
v0.4.0
Published
Adventist Inbox Custom Channel SDK - connect to Adventist Inbox chat with your own UI
Maintainers
Readme
adventist-inbox-sdk
JavaScript/TypeScript SDK to connect your own UI to the Adventist Inbox chat platform via a Custom Channel. Use the channel connection key and secret to send messages over REST and receive agent replies in real time over Socket.IO. This document is a granular, step-by-step reference.
Table of contents
- Overview
- Install
- Configuration reference
- Step-by-step usage
- connect() in detail
- Sending messages
- Delivery and read status
- Receiving messages (real-time)
- getMessageWindow
- getConversation (deprecated)
- Media messaging
- Media URL helpers
- Events reference
- TypeScript types
- Idempotency
- Constraints and validation
- Authentication
- Troubleshooting
- Related docs
Overview
- What it does: One client for (1) sending contact messages (text and media), (2) reporting delivery/read status, (3) loading history via
getMessageWindow, (4) receiving agent messages and status updates in real time when you passsessionIdin config. - When to use: You have a Custom Channel in Adventist Inbox and want to build your own chat UI (web or Node) without implementing Socket.IO room logic or raw REST yourself.
- Real-time vs REST-only: If you provide
sessionIdin config and callconnect(), the SDK opens Socket.IO to/customand joins a session room. Agent replies arrive viamessage; status ticks viamessage_status; media finalize patches viamessage_media_ready. If you omitsessionId, the client is REST-only (send and pollgetMessageWindow()).
Install
pnpm add adventist-inbox-sdk
# or
npm install adventist-inbox-sdkBuilt with tsup (CJS dist/index.js, ESM dist/index.mjs, UMD dist/index.umd.js, types dist/index.d.ts).
Browser (UMD):
<script src="https://unpkg.com/[email protected]/dist/index.umd.js"></script>Then use AdventistInbox.AdventistInboxClient (global name AdventistInbox).
Node.js: Use ESM; ensure a fetch implementation and WebSocket support are available if you use real-time.
Configuration reference
| Field | Type | Required | Description |
| ----------- | -------- | -------- | ----------- |
| baseUrl | string | Yes | Adventist Inbox API base URL (e.g. https://api.example.com). No trailing slash. |
| channelId | string | Yes | Custom Channel connection key (from Adventist Inbox when you create the channel). Do not use an internal numeric ID. |
| secret | string | Yes | Channel secret. Shown once in the webapp; store securely. Sent as X-AWR-Channel-Secret. |
| sessionId | string | No* | Unique identifier for this visitor/session. Required for real-time receive. Must not contain :. |
| visitorName | string | No | Display name for the contact in the inbox. |
| requestId | string | No | Correlation ID sent as X-Request-Id; API echoes it in logs and responses. |
*Omit for REST-only mode; include for real-time agent messages.
Step-by-step usage
- Create the client with at least
baseUrl,channelId, andsecret. For real-time receive, also setsessionId(and optionallyvisitorName). - Attach event listeners before calling
connect():connection_ready,connection_error,message,message_status,message_media_ready,reconnecting,reconnected,error. - Call
connect(). IfsessionIdis set, the SDK opens Socket.IO and joins the session room. When the server acknowledges join,connection_readyfires (with no payload). - Send / upload / status only after
connection_ready(or afterconnect()in REST-only mode). Always passoptions.sessionIdtosendMessage()/uploadMedia()/sendMessageStatus(). - Load history with
getMessageWindow(sessionId, { intent, limit, beforeSeq, afterSeq, aroundSeq, aroundMessageId }). The client must be connected first. - Receive agent messages on
message; update ticks onmessage_status; patch media bubbles onmessage_media_ready.
Minimal real-time example:
import { AdventistInboxClient } from "adventist-inbox-sdk";
const sessionId = "user-123";
const client = new AdventistInboxClient({
baseUrl: "https://your-api.example.com",
channelId: "your-connection-key",
secret: "your-channel-secret",
sessionId,
visitorName: "Alice",
});
client.on("connection_ready", () => {
console.log("Ready");
});
client.on("connection_error", (err) => console.error(err));
client.on("message", (msg) => console.log("Agent:", msg.textMessage, msg.mediaUrl));
client.on("message_status", (payload) => console.log("Status:", payload));
client.on("message_media_ready", (msg) => console.log("Media ready:", msg._id, msg.mediaUrl));
client.on("reconnecting", () => console.log("Reconnecting…"));
client.on("reconnected", () => console.log("Reconnected"));
client.on("error", (err) => console.error(err));
client.connect();
// Later, after connection_ready:
const result = await client.sendMessage("Hello!", { sessionId, visitorName: "Alice" });
console.log(result.socialMessageId);connect() in detail
What it does: Validates config (
baseUrl,channelId,secretrequired). Sets the client to “connected” sosendMessageandgetMessageWindowcan run. IfsessionIdis set, opens Socket.IO to{baseUrl}/custom, sends auth andcustom:join, and waits for server acknowledgment.Join timeout: If the server does not acknowledge the join within 10 seconds, the SDK emits
connection_error(anderror) with a timeout message.connection_ready: Fires when the server has accepted the join (first connect only). No payload. Only after this should you send / upload / report status in real-time mode.
connection_error: Fires when Socket.IO connect/auth or join fails. Common causes: wrong
secret, wrongchannelId(e.g. numeric ID instead of connection key), orsessionIdcontaining:.Reconnect: On transport reconnect, the SDK re-joins the room, emits
reconnectingthenreconnected(not anotherconnection_ready).Order: Register all listeners, then call
connect(). Do not send beforeconnection_readyin real-time mode.disconnect()– Close Socket.IO (if open) and mark client disconnected.setConfig(config)– Set or replace config beforeconnect().isConnected()– Whetherconnect()has been called and not yet disconnected.isRealtimeReady()– WhensessionIdis set, returnstrueonly aftercustom:joinsucceeds.sendMessage,uploadMedia, andsendMessageStatuswait for this automatically.getMessageWindow/getConversationdo not wait.getChannelInfo()– GET channel details (secret not included in response). Client must be connected.
Sending messages
Signature: sendMessage(textOrContent: string, options: SendMessageOptions): Promise<{ success: boolean; message?: string; socialMessageId: string }>.
- options.sessionId is required. Must not contain
:. - For text: pass the message as the first argument; optional
visitorName,socialMessageId. - For media: set
messageType: "media", requiredmediaUrl, and optionallymediaType('image' | 'audio' | 'video' | 'document'). The first argument is the optional caption. - If you omit
socialMessageId, the SDK auto-generates one (UUID when available) and always returns it.
SDK checks (before sending):
- Client is connected; otherwise throws
"Client not connected. Call connect(config) first.". - Real-time join is ready when
sessionIdwas set on config (waits automatically). options.sessionIddoes not contain:; otherwise throws.- If
messageType === 'media',mediaUrlmust be set; otherwise throws.
Return value:
- On HTTP 2xx:
{ success: true, socialMessageId }. - On HTTP 4xx/5xx:
{ success: false, message?, socialMessageId }. The SDK also emitserror.
Example (text):
const result = await client.sendMessage("Your order has shipped", {
sessionId: "user-123",
visitorName: "Alex",
socialMessageId: "order-123-msg-1", // optional; auto-generated if omitted
});
if (!result.success) console.error(result.message);
else console.log("Sent as", result.socialMessageId);Example (media with caption):
await client.sendMessage("Invoice attached", {
sessionId: "user-123",
messageType: "media",
mediaType: "image",
mediaUrl: "https://cdn.example.com/invoice.png",
socialMessageId: "upload-inv-001",
});Request body: The SDK POSTs to /webhook/custom with a single message object: senderType: "CONTACT", messageType ("TEXT" or "media"), textMessage, socialMessageId, metadata (including sessionId, visitorName, customChannelId), and for media mediaUrl and mediaType. The API expects this shape.
Delivery and read status
Contacts report delivery/read so the inbox can show ticks. Agent → contact status arrives on the message_status event.
sendMessageStatus(options)
await client.sendMessageStatus({
sessionId: "user-123",
socialMessageId: "agent-msg-id-1",
messageType: "READ_UPDATE", // or "DELIVERY_UPDATE" | "STATUS_UPDATE"
status: "READ", // or "DELIVERED" | "SENT" | "FAILED"
visitorName: "Alice",
});Returns { success: boolean; message?: string }. Waits for realtime readiness when a session is configured.
markMessagesRead(sessionId, socialMessageIds, visitorName?)
const { success, failed } = await client.markMessagesRead(
"user-123",
["agent-msg-1", "agent-msg-2"],
"Alice"
);getMessageStatus(sessionId, socialMessageId)
Returns the last cached delivery status ('sent' | 'delivered' | 'read' | 'failed') from local sends, history, or message_status events. undefined if unknown.
Receiving messages (real-time)
- When: Only when you passed
sessionIdin config and the Socket.IO join succeeded. - Register listeners before
connect()so you do not miss events. message: Normalized message from servermessage:sent(agent/content for this session). Preferseqfor ordering when present.message_media_ready: Fired when background media finalize completes. Patch the existing bubble by_id/socialMessageId.message_status:{ socialMessageId, status, messageType?, sessionId?, timestamp? }for delivery/read ticks on messages you sent.- If
messagenever fires: Confirmconnection_readyfired; agent replying to the samesessionId; listeners attached beforeconnect(); same client instance.
client.on("message", (msg) => {
if (msg.messageType === "text" || !msg.mediaUrl) {
addBubble(msg.textMessage ?? "");
return;
}
if (msg.mediaType === "image") addImage(getMessageDisplayUrl(msg), msg.textMessage);
else if (msg.mediaType === "audio") addAudio(getMessageFullUrl(msg));
else if (msg.mediaType === "video") addVideo(getMessageFullUrl(msg));
else if (msg.mediaType === "document") addDocumentLink(getMessageFullUrl(msg), msg.textMessage);
});
client.on("message_media_ready", (msg) => {
patchMediaBubble(msg._id, { mediaUrl: msg.mediaUrl, thumbnail: msg.thumbnail });
});
client.on("message_status", ({ socialMessageId, status }) => {
updateTicks(socialMessageId, status);
});getMessageWindow
Signature: getMessageWindow(sessionId: string, options?: GetMessageWindowOptions): Promise<CanonicalMessageWindowResponse>.
Calls GET /webhook/custom/channels/:channelId/contact/:sessionId/messages/window with X-AWR-Channel-Secret.
- options:
{ intent?: 'present' | 'older' | 'newer' | 'message', limit?: number, beforeSeq?: number, afterSeq?: number, aroundSeq?: number, aroundMessageId?: string }. Defaultintentis'present'. - Requirement: The client must be connected (call
connect()first). - sessionId: Must not contain
:.
Return: { messages: CanonicalMessage[], oldestSeq, newestSeq, hasOlder, hasNewer, anchor }. Use hasOlder / hasNewer with beforeSeq / afterSeq to page by seq, not by page number.
Non-OK responses (including 404 when the window API is disabled or the conversation is missing) throw.
client.connect();
const present = await client.getMessageWindow(sessionId, { intent: "present", limit: 50 });
renderHistory(present.messages);
if (present.hasOlder && present.oldestSeq != null) {
const older = await client.getMessageWindow(sessionId, {
intent: "older",
limit: 50,
beforeSeq: present.oldestSeq,
});
prependHistory(older.messages);
}getConversation (deprecated)
Deprecated in 0.3.0. Use getMessageWindow instead. Page pagination will be removed in 0.4.0.
Signature: getConversation(sessionId: string, options?: GetConversationOptions): Promise<GetConversationResponse>.
- options:
{ page?: number, limit?: number }. Defaults:page = 1,limit = 20.limitis capped at 100 (API maximum). - Requirement: The client must be connected (call
connect()first). - sessionId: Must not contain
:.
Return: { messages: SimplifiedMessage[], pagination: Pagination }. Pagination includes currentPage, limit, totalMessages, totalPages, hasNextPage, hasPrevPage, nextPage, prevPage.
404: If the API returns 404 (no conversation for that session), the SDK returns empty messages and a default pagination object; it does not throw. Treat as empty history.
loadAllConversationMessages is also deprecated; walk getMessageWindow with intent: 'older' instead.
client.connect();
const all = [];
let page = 1;
while (true) {
const res = await client.getConversation(sessionId, { page, limit: 50 });
all.push(...res.messages);
if (!res.pagination.hasNextPage) break;
page = res.pagination.nextPage ?? page + 1;
}
renderHistory(all);Media messaging
Sending:
- Set
messageType: "media"and provide a reachablemediaUrl(prefer URLs fromuploadMedia/ the API). mediaType:'image' | 'audio' | 'video' | 'document'.- First argument to
sendMessage()is the optional caption. - Use
socialMessageIdfor idempotency on retries (or keep the auto-generated id from the return value).
Uploading a file: uploadMedia(file, options) uses the Custom Channel upload-intent → S3 → complete → finalize flow:
POST /webhook/custom/channels/:channelId/media/upload-intents(JSON:sessionId,mediaType,mimeType,sizeBytes,filename)- Direct upload to S3 (single PUT or multipart) using the returned plan
POST /webhook/custom/channels/:channelId/media/upload-intents/:intentId/complete- Poll until finalize returns
mediaUrl
Returns { mediaUrl, mediaType, thumbnail?, metadata? }. Then call sendMessage with messageType: "media". Options: sessionId, mediaType ('image' | 'audio' | 'document'), visitorName?, requestId?, onProgress?. Rate limits and file type/size limits apply on the API.
Example (upload then send):
const { mediaUrl, mediaType } = await client.uploadMedia(fileInput.files[0], {
sessionId: "user-session-123",
mediaType: "image",
visitorName: "Alice",
});
await client.sendMessage("Optional caption", {
sessionId: "user-session-123",
messageType: "media",
mediaUrl,
mediaType,
});Receiving:
- Detect media with
msg.messageType === "media". Usemsg.mediaType/msg.mediaUrl/msg.thumbnail. Prefer media URL helpers. Listen formessage_media_readywhen finalize is deferred. History and socket payloads may use time-limited (pre-signed) URLs — treat them as display links within the API’s expiry window.
Media URL helpers
Exported from the package entry:
| Helper | Purpose |
| ------ | ------- |
| getMessageDisplayUrl(message) | Preview URL: thumbnail if present, else durable mediaUrl (or in-flight blob while upload is pending). |
| getMessageFullUrl(message) | Full-resolution mediaUrl (not thumbnail). |
| resolveDurableMediaUrl(incoming, existing) | When patching a bubble from a socket update, prefer a non-blob/non-data URL. |
Also exported: toSimplifiedMessage(payload) — normalizes an API document or message:sent payload.
import {
AdventistInboxClient,
getMessageDisplayUrl,
getMessageFullUrl,
resolveDurableMediaUrl,
} from "adventist-inbox-sdk";Events reference
| Event | When it fires | Payload |
| ---------------------- | -------------------------------------------------- | ---------------------- |
| connection_ready | First successful Socket.IO join (or REST-only ready) | none |
| connection_error | Connect or join failed | Error |
| reconnecting | Socket.IO reconnection attempt | none |
| reconnected | Room re-joined after reconnect | none |
| message | Agent/content message for this session | message payload |
| message_media_ready | Background media finalize completed | message payload |
| message_status | Delivery/read status for a socialMessageId | MessageStatusPayload |
| error | Request/connection errors | Error |
Subscribe with client.on(eventName, handler).
TypeScript types
interface AdventistInboxConfig {
baseUrl: string;
channelId: string;
secret: string;
sessionId?: string;
visitorName?: string;
requestId?: string;
}
interface SendMessageOptions {
sessionId: string;
visitorName?: string;
messageType?: 'TEXT' | 'media';
mediaUrl?: string;
mediaType?: 'audio' | 'image' | 'video' | 'document';
socialMessageId?: string | null; // auto-generated when omitted
}
type StatusMessageType = 'READ_UPDATE' | 'DELIVERY_UPDATE' | 'STATUS_UPDATE';
type MessageDeliveryStatus = 'sent' | 'delivered' | 'read' | 'failed';
interface SendMessageStatusOptions {
sessionId: string;
socialMessageId: string;
messageType: StatusMessageType;
status: 'READ' | 'DELIVERED' | 'SENT' | 'FAILED';
visitorName?: string;
}
interface MessageStatusPayload {
socialMessageId: string;
status: MessageDeliveryStatus;
messageType?: StatusMessageType;
sessionId?: string;
timestamp?: string;
}
interface GetConversationOptions {
page?: number;
limit?: number;
}
interface GetMessageWindowOptions {
intent?: 'present' | 'older' | 'newer' | 'message';
limit?: number;
beforeSeq?: number;
afterSeq?: number;
aroundSeq?: number;
aroundMessageId?: string;
}
interface CanonicalMessage {
_id: string;
seq: number | null;
senderType: 'CONTACT' | 'AGENT' | 'SYSTEM';
messageType: string;
textMessage?: string;
mediaType?: string;
mediaUrl?: string;
thumbnail?: string;
status?: string;
socialMessageId?: string | null;
buttons?: MessageButton[];
metadata?: Record<string, unknown>;
createdAt?: string;
updatedAt?: string;
visibility: 'customer' | 'agent';
}
interface CanonicalMessageWindowResponse {
messages: CanonicalMessage[];
oldestSeq: number | null;
newestSeq: number | null;
hasOlder: boolean;
hasNewer: boolean;
anchor: { intent: string; seq: number | null; messageId: string | null };
}
interface SimplifiedMessage {
_id: string;
senderType: 'CONTACT' | 'AGENT';
messageType: string;
textMessage?: string;
mediaType?: string;
mediaUrl?: string;
thumbnail?: string;
status?: string;
socialMessageId?: string | null;
/** Conversation-scoped sequence from API / getMessageWindow. */
seq?: number;
buttons?: MessageButton[];
visibility?: 'customer' | 'agent';
metadata?: Record<string, unknown>;
createdAt?: string;
updatedAt?: string;
}
interface Pagination {
currentPage: number;
limit: number;
totalMessages: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
nextPage: number | null;
prevPage: number | null;
}
interface GetConversationResponse {
messages: SimplifiedMessage[];
pagination: Pagination;
}
interface ChannelInfoResponse {
id: number;
name: string;
channelUid: string;
channelType: string;
channelConfig: Record<string, unknown>;
[key: string]: unknown;
}Idempotency
When the same send request might be retried (e.g. network or double-submit), pass a stable socialMessageId per logical message. The API deduplicates by this value. If you omit it, the SDK generates one and returns it — keep that value to correlate message_status events.
await client.sendMessage("Hello!", {
sessionId: "user-123",
socialMessageId: "my-unique-id-123",
});Constraints and validation
- sessionId: Must not contain the character
:. Used in internal identifiers on the API; colons break parsing. - channelId: Use the Custom Channel connection key from the webapp, not an internal numeric ID.
- The Custom Channel must have customChannelId (connection key) in its channel config in Adventist Inbox; otherwise the API returns 400 when receiving messages.
Authentication
All HTTP requests use the channel secret in the X-AWR-Channel-Secret header. Optional X-Request-Id is sent when you set requestId in config. Obtain the connection key and secret when creating the Custom Channel in the Adventist Inbox webapp; the secret is shown only once.
Troubleshooting
| Symptom | What to check |
| -------- | -------------- |
| Client not connected | Call connect() (or pass config into connect(config)). Do not call sendMessage or getMessageWindow before connect. |
| Real-time session is not ready | Wait for connection_ready (or let sendMessage / uploadMedia / sendMessageStatus await join). |
| sessionId must not contain ":" | Use an ID without colons (e.g. UUID, numeric id, or slug). |
| mediaUrl is required when messageType is "media" | Pass mediaUrl in options for media messages. |
| Custom channel not found / 404 | Wrong baseUrl, wrong channelId (use connection key, not numeric ID), or channel not created. |
| 401 Unauthorized | Wrong secret or missing X-AWR-Channel-Secret. |
| connection_error / join timeout | Wrong secret, wrong channelId, or sessionId contains :. Ensure API and Socket.IO are reachable. |
| message event never fires | Listeners registered before connect(); connection_ready fired; agent replying to the same session in the inbox; same client instance (no re-create). |
| message_status never fires | Listen before connect(); agent opened/read the conversation; SDK ≥ 0.2.0. |
| Media bubble stuck without URL | Listen for message_media_ready; use getMessageDisplayUrl / resolveDurableMediaUrl. |
| CORS errors (browser) | Configure CORS on the Adventist Inbox API to allow your origin. |
| No messages in getMessageWindow | Same sessionId as used for send; allow a moment for persistence. |
Related docs
- TESTING.md – How to test the SDK against a running Adventist Inbox API (env vars, scripts, curl, troubleshooting).
- PUBLISHING.md – How to publish the package to npm (maintainers).
