Developer guide
Add two-way messaging to a Node.js SaaS app
A two-way customer messaging integration needs an outbound API call, a verified inbound webhook and persistent conversation state. Queue work, deduplicate events, and distinguish message acceptance from delivery.
Run the Node.js example
Download and extract the example. It uses Node.js 22.13 or later, built-in HTTP and SQLite, and no third-party dependencies. The send simulator runs locally and never texts a real customer.
For a real test, export COMMS_API_KEY and COMMS_TEST_TO, choosing a recipient who agreed to the test. Run send.mjs with a stable COMMS_IDEMPOTENCY_KEY. Do not commit the key or expose it in browser code.
cd comms-node
node --test test.mjs
node simulate.mjs
# Real API test, only with a consenting recipient:
node --env-file=.env send.mjsSend a customer message
Use a scoped server-side key and an E.164 recipient. Save the message ID from the response. If the request times out, inspect state or retry the same request with the same idempotency key; do not create a new key merely because a receipt is slow.
const response = await fetch("https://osis.co/api/v1/comms/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COMMS_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "appointment-4242-confirm"
},
body: JSON.stringify({
to: process.env.COMMS_TEST_TO,
body: "Your appointment is confirmed.",
channel: "auto"
})
});
if (!response.ok) throw new Error(`Comms returned ${response.status}`);
const result = await response.json();
console.log(result.message.id); // acceptance is not deliveryVerify and persist inbound events
Register a public HTTPS webhook at /webhooks. Save the returned whsec_ secret and supply it to the receiver as COMMS_WEBHOOK_SECRET. The example serves POST /webhooks/comms, verifies the raw bytes before parsing, and persists a unique event ID before acknowledging.
Events include id, event, recordId and data.message. Route data.message.line_e164 through a trusted configuration to identify the tenant. Unknown lines and mismatched conversations are rejected. Multiple workers in production must share the durable inbox; this example is one process.
POST https://osis.co/api/v1/comms/webhooks
{
"url": "https://your-app.example/webhooks/comms",
"events": ["comms.message.received", "comms.message.delivered"]
}Track delivery without assuming read state
Acceptance is not delivery. Delivery and read events depend on the channel, line, recipient, and available receipts. SMS does not provide read receipts.
Persist delivery status by message ID. The sample processes inbound and delivery events separately and never treats an accepted request as proof that the recipient saw it. Read receipts can be unavailable or disabled.
Isolate tenants and control automated sends
Use a unique line-to-tenant mapping, reject unknown lines, and scope API credentials to the intended integration. The example stores conversation ownership as ai or human and keeps an opt-out flag.
It deliberately does not execute instructions from an inbound message or automatically text arbitrary numbers.
Before scaling, add your production queue, retention policy, observable failures, provider-state reconciliation and a reviewed consent policy. Test event replay, persistence after restart and a human takeover.