npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

adventist-inbox-sdk

v0.4.0

Published

Adventist Inbox Custom Channel SDK - connect to Adventist Inbox chat with your own UI

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

  • 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 pass sessionId in 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 sessionId in config and call connect(), the SDK opens Socket.IO to /custom and joins a session room. Agent replies arrive via message; status ticks via message_status; media finalize patches via message_media_ready. If you omit sessionId, the client is REST-only (send and poll getMessageWindow()).

Install

pnpm add adventist-inbox-sdk
# or
npm install adventist-inbox-sdk

Built 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

  1. Create the client with at least baseUrl, channelId, and secret. For real-time receive, also set sessionId (and optionally visitorName).
  2. Attach event listeners before calling connect(): connection_ready, connection_error, message, message_status, message_media_ready, reconnecting, reconnected, error.
  3. Call connect(). If sessionId is set, the SDK opens Socket.IO and joins the session room. When the server acknowledges join, connection_ready fires (with no payload).
  4. Send / upload / status only after connection_ready (or after connect() in REST-only mode). Always pass options.sessionId to sendMessage() / uploadMedia() / sendMessageStatus().
  5. Load history with getMessageWindow(sessionId, { intent, limit, beforeSeq, afterSeq, aroundSeq, aroundMessageId }). The client must be connected first.
  6. Receive agent messages on message; update ticks on message_status; patch media bubbles on message_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, secret required). Sets the client to “connected” so sendMessage and getMessageWindow can run. If sessionId is set, opens Socket.IO to {baseUrl}/custom, sends auth and custom:join, and waits for server acknowledgment.

  • Join timeout: If the server does not acknowledge the join within 10 seconds, the SDK emits connection_error (and error) 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, wrong channelId (e.g. numeric ID instead of connection key), or sessionId containing :.

  • Reconnect: On transport reconnect, the SDK re-joins the room, emits reconnecting then reconnected (not another connection_ready).

  • Order: Register all listeners, then call connect(). Do not send before connection_ready in real-time mode.

  • disconnect() – Close Socket.IO (if open) and mark client disconnected.

  • setConfig(config) – Set or replace config before connect().

  • isConnected() – Whether connect() has been called and not yet disconnected.

  • isRealtimeReady() – When sessionId is set, returns true only after custom:join succeeds. sendMessage, uploadMedia, and sendMessageStatus wait for this automatically. getMessageWindow / getConversation do 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", required mediaUrl, and optionally mediaType ('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):

  1. Client is connected; otherwise throws "Client not connected. Call connect(config) first.".
  2. Real-time join is ready when sessionId was set on config (waits automatically).
  3. options.sessionId does not contain :; otherwise throws.
  4. If messageType === 'media', mediaUrl must be set; otherwise throws.

Return value:

  • On HTTP 2xx: { success: true, socialMessageId }.
  • On HTTP 4xx/5xx: { success: false, message?, socialMessageId }. The SDK also emits error.

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 sessionId in config and the Socket.IO join succeeded.
  • Register listeners before connect() so you do not miss events.
  • message: Normalized message from server message:sent (agent/content for this session). Prefer seq for 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 message never fires: Confirm connection_ready fired; agent replying to the same sessionId; listeners attached before connect(); 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 }. Default intent is '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. limit is 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 reachable mediaUrl (prefer URLs from uploadMedia / the API).
  • mediaType: 'image' | 'audio' | 'video' | 'document'.
  • First argument to sendMessage() is the optional caption.
  • Use socialMessageId for 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:

  1. POST /webhook/custom/channels/:channelId/media/upload-intents (JSON: sessionId, mediaType, mimeType, sizeBytes, filename)
  2. Direct upload to S3 (single PUT or multipart) using the returned plan
  3. POST /webhook/custom/channels/:channelId/media/upload-intents/:intentId/complete
  4. 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". Use msg.mediaType / msg.mediaUrl / msg.thumbnail. Prefer media URL helpers. Listen for message_media_ready when 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).