> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.pinnacle.sh/v-2/quickstart/rcs/typescript/receive/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pinnacle.sh/_mcp/server. # Receiving RCS Messages > Receiving your first RCS message with Pinnacle's Typescript SDK ```typescript import express from "express"; import { Pinnacle, PinnacleClient } from "rcs-js"; import { config } from "dotenv"; config(); const port = 3000; const app = express(); const client = new PinnacleClient({ apiKey: process.env.PINNACLE_API_KEY }); app.get("/send-rcs/:phoneNumber", async (req, res) => { const { phoneNumber } = req.params; const result = await client.messages.rcs.send({ options: { validate: true, // Will let you know if your RCS message will fail to be sent ahead of making the request to carriers }, from: process.env.AGENT_ID || "", to: phoneNumber, cards: [ { media: "https://server.trypinnacle.app/storage/v1/object/sign/vault/3/67330f56-8fc4-4d9e-882d-161b84fc8e31/Your_image_here.png?token=eyJraWQiOiJzdG9yYWdlLXVybC1zaWduaW5nLWtleV9hOGI0YTI0NC00NzY4LTRhOTktYWI4MS1iNmZhNTZhNGQyZWYiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJ2YXVsdC8zLzY3MzMwZjU2LThmYzQtNGQ5ZS04ODJkLTE2MWI4NGZjOGUzMS9Zb3VyX2ltYWdlX2hlcmUucG5nIiwiaWF0IjoxNzYwOTg0OTEwLCJleHAiOjMxNzEyMDk4NDkxMH0.bcIMtiBAvV8C7Gw7uYaR5TMGsCep7w1TvRMjoRlP_-g", title: "Hello, world!", subtitle: "This is an example RCS message with rich content.", buttons: [ { type: "trigger", metadata: "", payload: "HELLO", title: "Say hello back", }, { type: "sendLocation", latLong: { lat: 36.7749, lng: -122.4194, }, metadata: "", title: "View example location", }, ], }, { media: "https://server.trypinnacle.app/storage/v1/object/sign/vault/3/67330f56-8fc4-4d9e-882d-161b84fc8e31/Your_image_here.png?token=eyJraWQiOiJzdG9yYWdlLXVybC1zaWduaW5nLWtleV9hOGI0YTI0NC00NzY4LTRhOTktYWI4MS1iNmZhNTZhNGQyZWYiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJ2YXVsdC8zLzY3MzMwZjU2LThmYzQtNGQ5ZS04ODJkLTE2MWI4NGZjOGUzMS9Zb3VyX2ltYWdlX2hlcmUucG5nIiwiaWF0IjoxNzYwOTg0OTEwLCJleHAiOjMxNzEyMDk4NDkxMH0.bcIMtiBAvV8C7Gw7uYaR5TMGsCep7w1TvRMjoRlP_-g", title: "Your second card", subtitle: "This subtitle is optional. Each card can have different buttons, like this one allowing you to share your location instead of view a location.", buttons: [ { type: "requestUserLocation", metadata: "", title: "Share your location", }, ], }, ], // Quick replies are available across all cards while buttons are specific to certain cards, and only show when a user swipes to that particular card quickReplies: [ { type: "scheduleEvent", title: "Add example event to cal", metadata: "", eventTitle: "Sample Event", eventStartTime: "2025-12-11T00:00:00Z", eventEndTime: "2025-12-11T23:59:59Z", eventDescription: "This is an example calendar event.", }, { type: "call", metadata: "", payload: "+14152321234", title: "Call example number", }, { type: "openUrl", metadata: "", payload: "https://docs.pinnacle.sh/api-reference/messages/send-rcs", title: "View RCS docs", }, ], }); res.json(result); }); app.post("/inbound-rcs", express.json(), async (req, res) => { try { // Process and validate the webhook // Returns a fully typed MessageEvent object const messageEvent: Pinnacle.MessageEvent | Pinnacle.UserEvent = await client.messages.process(req); console.log(messageEvent); // messageEvent is now typed as Pinnacle.MessageEvent // Your business logic here await handleInboundMessage(messageEvent); res.status(200).json({ message: "You received a message" }); } catch (error) { console.error("Error occurred processing webhook message", error); res.status(200).json({ message: "You received a message" }); } }); async function handleInboundMessage( event: Pinnacle.MessageEvent | Pinnacle.UserEvent ) { // event is fully typed with autocomplete support // Your message handling logic // Check if the message contains a button click with HELLO payload // View full docs on processing events here: https://docs.pinnacle.sh/methods/process if (event.type === "MESSAGE.RECEIVED") { if (event.direction === "INBOUND") { console.log("event:", event.message); const message = event.message; // Check if this is RCS button data (discriminated by type field) if ( message.type === "RCS_BUTTON_DATA" && message.button.payload === "HELLO" ) { await sendHello(event.conversation.from); } } } } async function sendHello(to: string) { const result = await client.messages.rcs.send({ text: "Hello! Button clicked successfully.", from: process.env.AGENT_ID || "", to: to, quickReplies: [], }); console.log(result); } app.listen(port, () => { console.log(`Server is listening on port ${port}`); }); ``` ## Prerequisites Before proceeding, ensure you have obtained an RCS sandbox agent and API key as described in the [prerequisites](/quickstart/rcs). ## Installation Initialize a new Node.js project: ```bash npm init -y ``` Install the Pinnacle TypeScript SDK, Express, and dotenv: ```bash npm install rcs-js express dotenv ``` Install development dependencies: ```bash npm install --save-dev @types/express @types/node tsx ``` > **Info** > > This guide uses version `rcs-js>=2.0.3`. It's compatible with the following > runtimes: Node.js 18+, Vercel, Cloudflare Workers, Deno v1.25+, Bun 1.0+, and > React Native. ## Configuration Create an `.env` file in your project root and add your Pinnacle API key and signing secret: ``` PINNACLE_API_KEY="your_api_key" # pnclk_ AGENT_ID="your_agent_id" # agent_ PINNACLE_SIGNING_SECRET="your_signing_secret" # pss_ ``` ## Setting Up a Webhook To receive inbound RCS messages, you need to configure a webhook in the Pinnacle dashboard: 1. Navigate to **Development > Webhooks** in the Pinnacle dashboard 2. Click **Create new webhook** 3. Give your webhook a descriptive name 4. Enter your webhook endpoint URL * For local development, use an ngrok tunnel pointing to `localhost:3000/inbound-rcs` * For production, use your deployed server URL 5. After creation, copy the **signing secret** and add it to your `.env` file 6. Add your RCS sandbox agent to your webhook for it to receive messages. You must also whitelist the devices you want to test with by navigating to your sandbox agent and adding test device phone numbers. > **Info** > > Optionally, you can configure custom HTTP headers (e.g. `X-API-KEY`) to be > sent on every webhook delivery. Add them in the dashboard or via the > `headers` field on [`POST /webhooks/attach`](/api-reference/webhooks/attach-webhook). > The `PINNACLE-SIGNING-SECRET` header is reserved. ## Creating Your Webhook Endpoint Create a new TypeScript file (e.g., `index.ts`) and add the following snippet to the right. The code above creates an Express endpoint that: * Receives webhook POST requests at `/inbound-rcs` * Verifies the webhook signature using your signing secret * Processes incoming message events and replies to a button press by the user ## Running Your Server Start the Express server: ```bash npx tsx index.ts ``` Your server will start on `http://localhost:3000`. If you're using ngrok for local development, start it in a separate terminal: ```bash ngrok http 3000 ``` Use the ngrok URL (e.g., `https://abc123.ngrok.io/inbound-rcs`) as your webhook endpoint in the Pinnacle dashboard. ## Testing Your Webhook Go to `localhost:3000/send-rcs/+12345678910` (e.g., to your whitelisted number). If there are no errors, you should see something like ```json { "message_ids": null, "segments": 1, "total_cost": 0.03, "sender": "agent_exampleId", "recipient": "+12345678910", "status": "queued", "messageId": 6828 } ``` and receive a message on your whitelisted device like this: ![rcs message](https://server.trypinnacle.app/storage/v1/object/sign/vault/3/5f7289f9-8a7e-4c5c-9d51-776712ef6f8c/IMG_7658.jpg?token=eyJraWQiOiJzdG9yYWdlLXVybC1zaWduaW5nLWtleV9hOGI0YTI0NC00NzY4LTRhOTktYWI4MS1iNmZhNTZhNGQyZWYiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJ2YXVsdC8zLzVmNzI4OWY5LThhN2UtNGM1Yy05ZDUxLTc3NjcxMmVmNmY4Yy9JTUdfNzY1OC5qcGciLCJpYXQiOjE3NjA5ODU5OTcsImV4cCI6MzE3MTIwOTg1OTk3fQ.5s3C9oHuU-jJKWRlihedlD_xALKLmxxn4ktXiwRvMkU) > **Warning** > > If you're not receiving any messages, make sure you have > > * Your RCS sandbox agent associated with your webhook > * Your test device is whitelisted If you tap "Say hello back", your whitelisted device should receive a text saying "Hello! Button clicked successfully." With that, your webhook should now be successfully receiving inbound RCS messages as well message status updates for outbound messages! You can monitor these statuses by filtering events by their message direction set to outbound: ```typescript if (event.type === "MESSAGE.RECEIVED" && event.direction === "OUTBOUND") { console.log(event); } ``` For more detail about processing the message payload received, please view the [process method](/methods/process). > One API for RCS, iMessage, MMS, and SMS. Build, test, and scale every channel — send your first message in minutes, not weeks.