A purple robot reading a story time book, with a bubble next to it that writes Gemini inside.

Build a Storytelling Service With RCS Rich Cards and Gemini

最終更新日 July 30, 2025

所要時間:6 分

Introduction

This tutorial shows you how to build a generative AI storytelling service using the Vonage Messages API for RCS and Google’s Gemini AI. You'll learn what RCS is, how to send and receive RCS Rich Card messages, and how to integrate Gemini to generate short bedtime stories.

Inspired by my toddler's bedtime routines, I wanted to create something useful. So I combined RCS messaging with Gemini AI to develop a simple storytelling service.

An android phone view showcasing an image that that shows Gemini and tell me a story with a generate story buttonRCS Story Demonstration

You can find the complete source code on the Vonage Community GitHub repository.

Prerequisites

How to Get RCS Working on Your Phone

To send and receive RCS messages in your Vonage application, you need a registered RBM agent and a phone with RCS capabilities.

RCS through Vonage is currently only available for managed accounts. You’ll need to contact your account manager to activate Developer Mode, which allows testing with allowlisted numbers.

If you don't have a managed account, reach out to the Vonage Sales Team for support.

Terminology

RCS (Rich Communication Services): An industry standard that enhances traditional messaging with rich media, branded content, and read receipts. If a recipient’s device or network doesn’t support RCS, messages automatically fall back to SMS to ensure they are still delivered.

Learn more in our RCS documentation.

SMS (Short Message Service): Traditional SMS supports only plain text. RCS allows branded buttons, images, and message status updates. Learn to send SMS messages with Node.js.

MMS (Multimedia Messaging Service): Unlike MMS, which often has lower media quality and file size limits, RCS supports high-quality media and features such as typing indicators and delivery status updates. Learn what you can do with MMS and how to send MMS with Node.js.

  • To create an application, go to the Create an Application page on the Vonage Dashboard, and define a Name for your Application.

  • If needed, click on "generate public and private key". A private key (.key file) will be generated. Download and store it securely. This key is needed for authentication when making API requests. Note: Private keys will not work unless the application is saved.

  • Choose the capabilities you need (e.g., Voice, Messages, RTC, etc.) and provide the required webhooks (e.g., event URLs, answer URLs, or inbound message URLs). These will be described in the tutorial.

  • To save and deploy, click "Generate new application" to finalize the setup. Your application is now ready to use with Vonage APIs.

Messages API Capability

Start an ngrok Tunneling Instance

If you haven’t used ngrok before, you can start here. I’ve added the steps to run an instance below the image in the next section.

Run the following command to start an ngrok tunneling instance.

ngrok http 3000

In the output, you’ll find a URL that starts with https:// and ends in .ngrok.app/. Make a note of it as you’ll need it for the next step to configure your webhooks. 

Forwarding https://94cd51b63460.ngrok.app/ -> http://localhost:8000          

Configure Webhooks 

Back in the Vonage API Dashboard, under your Apps, toggle the Messages option under capabilities and set your server's public URL from ngrok as the endpoint for inbound messages and events. it should look like: https://94cd51b63460.ngrok.app/webhooks/inbound for inbound and https://94cd51b63460.ngrok.app/webhooks/status for status. If you’d like to learn more about webhooks, you can find information on our Vonage documentation.

Inbound and Status URL with an ngrok generated url appended by /webhooks/inbound and /webhooks/stats respectively.webhooks

Initial Project Setup for Vonage Messages API

Clone the Vonage Community GitHub repository for this project and npm install the dependencies.

npm install express debug @vonage/messages @vonage/server-sdk dotenv

Create the Environment Variables File

Create a .env file in your project root. 

VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID, can be found within the application you’ve created

VONAGE_PRIVATE_KEY=./private.key, is the path to the private.key file that was generated when you created your application

RCS_SENDER_ID=YOUR_RCS_SENDER_ID

PHONE_NUMBER=YOUR_PHONE_NUMBER_E164 that will receive the RCS messages

PORT=3000

VONAGE_API_SIGNATURE_SECRET= is the secret used to sign the request that corresponds to the signature secret associated with the api_key included in the JWT claims. You can identify your signature secret on the Dashboard settings

In your project, you'll also find a vonage.js file that contains the Dotenv and Vonage Client Setup.

Add JWT Verification

Add a JWT verification function to verify if the incoming request (e.g., message or call) came from Vonage. 

The VONAGE_API_SIGNATURE_SECRET variable is the secret used to sign the request corresponding to the signature secret associated with the API key included in the JWT claims. You can identify your signature secret on the Dashboard settings

You can learn more about verifying the request.

const verifyJWT = (req) => {
  // Verify if the incoming request came from Vonage
  const jwtToken = req.headers.authorization.split(" ")[1];
  if(!verifySignature(jwtToken, process.env.VONAGE_API_SIGNATURE_SECRET)) {
    console.error("Signature does not match");
    throw new Error("Not a Vonage API request");
  }

  console.log("JWT verified");
}

Make sure to install the @vonage/jwt dependency.

npm install @vonage/jwt

Send an RCS Rich Card Message

In your server.js file, you'll find the finalized code with Gemini integration, but I wanted to give you a starting point on how to send a Rich Card Message in RCS with a hardcoded text so you can have a first experience with it and see it running before any more complex setup, with everything we've set up so far.

Start off by adding the imports and defining your variables.

import Express from "express";

import { RCSCustom, RCSText } from "@vonage/messages";

import { vonage } from "./vonage.js";

import "dotenv/config";

const app = new Express();

const port = process.env.PORT || 3000;

const recipientNumber = process.env.PHONE_NUMBER;

if (!recipientNumber) {

  process.exit(1);

}

if (!process.env.RCS_SENDER_ID) {

  process.exit(1);

}

We'll also use an async wrapper for error handling in routes.

const catchAsync = (fn) => (req, res, next) => {

  fn(req, res, next).catch(next);

};

app.use(Express.json());

app.get(

  "/send-story-request",

  catchAsync(async (req, res) => {

    await sendBedtimeStoryRequest(recipientNumber);

    res.status(200).json({ message: "Bedtime story request sent!" });

  })

);

Create the /webhooks/status endpoint.

app.post(

  "/webhooks/status",

  catchAsync(async (req, res) => {

    res.status(200).json({ ok: true });

  })

);

Create the /webhooks/inbound endpoint.

app.post(

  "/webhooks/inbound",

  catchAsync(async (req, res) => {

    const { channel, message_type, reply, from } = req.body;

    if (channel === 'rcs' && message_type === 'reply' && reply) {

      const receivedId = reply.id;

      const receivedTitle = reply.title;

      if (receivedId === 'GENERATE_STORY_REQUEST' || receivedTitle === 'Generate Story') {

        const replyToNumber = from;

        const helloWorldMessage = "Hello world!";

        await sendRCSGreeting(replyToNumber, helloWorldMessage);

      } else {

        // Unhandled reply

      }

    } else if (channel === 'rcs' && message_type === 'text') {

      if (req.body.text && req.body.text.toLowerCase() === 'generate story') {

        await sendRCSGreeting(from, "Hello world!");

      } else {

        await sendRCSGreeting(from, "I received your message: " + req.body.text);

      }

    }

    res.status(200).json({ ok: true });

  })

);

Add some error handling messages, and the app listens on the defined port (3000).

app.all("*", (req, res) => {

  res.status(404).json({

    status: 404,

    title: "Not Found",

  });

});

app.use((err, req, res, next) => {

  res.status(500).json({

    status: 500,

    title: "Internal Server Error",

    detail: err.message,

  });

});

app.listen(port);

Create the sendBedtimeStoryRequest that sends a new RCS Custom message that says ‘Bedtime Story Generator’, containing a button to generate a new story.

const sendBedtimeStoryRequest = async (number) => {

  const message = new RCSCustom({

    to: number,

    from: process.env.RCS_SENDER_ID,

    custom: {

      contentMessage: {

        richCard: {

          standaloneCard: {

            cardOrientation: "VERTICAL",

            cardContent: {

              title: "Bedtime Story Generator",

              description: 'Tap "Generate Story" for a magical tale!',

              media: {

                height: "MEDIUM",

                contentInfo: {

                  fileUrl:

                    "https://cdn-icons-png.flaticon.com/512/2917/2917637.png",

                },

              },

              suggestions: [

                {

                  reply: {

                    text: "Generate Story",

                    postbackData: "GENERATE_STORY_REQUEST",

                  },

                },

              ],

            },

          },

        },

      },

    },

  });

  try {

    await vonage.messages.send(message);

  } catch (err) {

    if (err.response) {

      err.response.text().catch(() => {});

    }

  }

};

Create the sendRCSGreeting that sends an RCS message using the Vonage Messages API.

const sendRCSGreeting = async (to, messageText) => {

  const message = new RCSText({

    to: to,

    from: process.env.RCS_SENDER_ID,

    text: messageText,

  });

  try {

    await vonage.messages.send(message);

  } catch (err) {

    if (err.response) {

      err.response.text().catch(() => {});

    }

  }

};

To run the app, start the server:

node server.js

Then visit the following URL in your browser, which triggers the first message to the user’s phone, inviting them to generate a story. The JSON confirmation in your browser, along with the received RCS message on your phone, are signs that the system is working.

http://localhost:3000/send-story-request

Integrate Google Gemini

Now that we have the RCS messaging and webhook working, let's add Google Gemini to our storytelling service. 

When you navigate to AI Studio, log in, and create a new chat, you can see ‘<\>Get Code' at the top right and select your programming language of choice. Choose the model. I've used “Gemini 1.5 Flash Experimental,” but you can choose the one that makes the most sense for your project. 

To learn more, visit the Gemini Developer API documentation page.

Install the Gemini SDK

npm install @google/generative-ai

Add your Gemini API key to .env.

GEMINI_API_KEY="YOUR_GEMINI_API_KEY"

Initialize Gemini 

import { GoogleGenerativeAI } from "@google/generative-ai";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);

const geminiModel = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });

Generate Stories with Gemini

With Gemini initialized, we update our/webhooks/inbound handler. When the user clicks "Generate Story," instead of replying "Hello world!", we now call geminiModel.generateContent with a specific prompt for a bedtime story. 

The generated text is then sent back to the user using sendGeneratedStory. We also added handling for users who might type "Generate Story" manually.

The sendGeneratedStory function now takes the generated story text and uses RCSText to send it back as a regular RCS text message.

Test the Storytelling Service

To run the app, (re)start the server:

node server.js

Then visit the following URL in your browser: http://localhost:3000/send-story-request

From your device, you'll receive an RCS message. Tap the "Generate Story" to receive a Gemini-created bedtime story.

A message is received on the phone device, the user clicks on the message, they click on the generate story button and then a story message is received.RCS Project

Conclusion

This tutorial showed you how to build a Gemini-powered storytelling service using Vonage’s RCS API. The last time I did anything related to RCS was in 2020. I participated in a workshop where each attendee was sent an RCS-enabled SIM card. It was nice to get back to working with RCS again, and I can't wait to write more blog posts about this.

Got any questions or comments? Join our thriving Developer Community on Slack, follow us on X (formerly Twitter), or subscribe to our Developer Newsletter. Stay connected, share your progress, and keep up with the latest developer news, tips, and events!

Share:

https://a.storyblok.com/f/270183/400x400/3f6b0c045f/amanda-cavallaro.png
Amanda CavallaroDeveloper Advocate