How to handle inbound RCS messages and taps when a user replies to your agent.
RCS is a two-way channel. When a user replies to your agent — by typing a message, tapping a suggested reply chip, or tapping a suggested action — Arowana fires a webhook event to your server.
There is no polling endpoint for inbound messages. You must register a webhook endpoint to receive user replies.
Once a user replies, you have 24 hours to send follow-ups using message_type: "CONVERSATION". Conversation messages are billed at the Conversation rate, not as individual Single Messages.
Keep conversation billing in mind. All CONVERSATION messages sent within the same 24-hour session count as a single Conversation billing unit — not per message.
After this event, sending another message to the same number requires a new outbound send (message_type: "MESSAGE" or "NEWSLETTER"), which starts a fresh conversation if the user replies again.
A minimal webhook handler in Node.js. This includes inline HMAC verification — see Webhooks — Signature verification for the standalone function:
const express = require("express");const crypto = require("crypto");const app = express();// IMPORTANT: Do NOT use express.json() globally.// The webhook route needs the raw Buffer to verify the HMAC signature.// Parsing JSON before verification changes the byte sequence and breaks it.app.post( "/webhooks/arowana", express.raw({ type: "application/json" }), async (req, res) => { const sig = req.headers["x-arowana-signature"]; const expected = "sha256=" + crypto .createHmac("sha256", process.env.WEBHOOK_SECRET) .update(req.body) // req.body is a Buffer here .digest("hex"); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) { return res.status(401).send("Invalid signature"); } const payload = JSON.parse(req.body.toString()); if (payload.event === "conversation.started") { const { from, user_message, agent_id } = payload.data; if (user_message.type === "suggested_reply") { const intent = Buffer.from(user_message.postback_data, "base64").toString(); await handleIntent(agent_id, from, intent); } else if (user_message.type === "text") { await handleFreeText(agent_id, from, user_message.text); } } res.status(200).send("OK"); });// Other routes that need JSON parsing should declare it on their own handler,// or register app.use(express.json()) only AFTER this route definition.// Never register a global JSON parser before the webhook route.
Send a message
Full guide to sending text, rich cards, and carousels.
Webhooks
Webhook setup, signature verification, and full event catalogue.