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.

How replies work

User taps "Track order"  →  carrier  →  Google RBM  →  Arowana  →  your webhook
  1. The user interacts with your message or types a free-text reply.
  2. Arowana receives the inbound event from the Google network.
  3. Arowana fires a conversation.started webhook to your endpoint.
  4. Your server has 24 hours to reply using message_type: "CONVERSATION" before the session closes.

Webhook event: conversation.started

Arowana sends this event for every inbound interaction — free-text replies, suggested reply taps, and action taps.
POST https://your-server.example.com/webhooks/arowana
Content-Type: application/json
X-Arowana-Signature: sha256=...

Free-text reply

When the user types a message:
{
  "id": "evt_f6g7h8",
  "event": "conversation.started",
  "timestamp": "2026-03-28T09:05:00Z",
  "data": {
    "agent_id": "ag_live_xxxx",
    "from": "+4917612345678",
    "session_expires_at": "2026-03-29T09:05:00Z",
    "user_message": {
      "type": "text",
      "text": "Where's my order?"
    }
  }
}

Suggested reply tap

When the user taps a reply chip you sent:
{
  "id": "evt_g7h8i9",
  "event": "conversation.started",
  "timestamp": "2026-03-28T09:05:10Z",
  "data": {
    "agent_id": "ag_live_xxxx",
    "from": "+4917612345678",
    "session_expires_at": "2026-03-29T09:05:10Z",
    "user_message": {
      "type": "suggested_reply",
      "text": "Track order",
      "postback_data": "dHJhY2tfMTIzNA=="
    }
  }
}
postback_data is the base64-encoded string you set when building the suggestion chip. Decode it server-side to recover your intent identifier.

Suggested action tap

When the user taps an action chip (e.g. dial, open URL):
{
  "id": "evt_h8i9j0",
  "event": "conversation.started",
  "timestamp": "2026-03-28T09:05:20Z",
  "data": {
    "agent_id": "ag_live_xxxx",
    "from": "+4917612345678",
    "session_expires_at": "2026-03-29T09:05:20Z",
    "user_message": {
      "type": "suggested_action",
      "postback_data": "b3Blbl93ZWJzaXRl"
    }
  }
}

Reply within the conversation window

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.
curl -sS -X POST "https://api.arowana.app/v1/messages" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "ag_live_xxxx",
    "to": "+4917612345678",
    "message_type": "CONVERSATION",
    "content": {
      "text": "Your order #1234 will arrive tomorrow between 10:00–14:00.",
      "suggestions": [
        { "reply": { "text": "Got it", "postback_data": "YWNr" } },
        { "reply": { "text": "Change address", "postback_data": "Y2hhbmdlX2FkZHI=" } }
      ]
    },
    "traffic_type": "TRANSACTION"
  }'
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.

Session expiry: conversation.ended

When 24 hours elapse without a user reply the session closes and Arowana fires:
{
  "id": "evt_i9j0k1",
  "event": "conversation.ended",
  "timestamp": "2026-03-29T09:05:00Z",
  "data": {
    "agent_id": "ag_live_xxxx",
    "with": "+4917612345678",
    "ended_at": "2026-03-29T09:05:00Z",
    "reason": "ttl_expired"
  }
}
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.

Handling inbound in your app

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.