Skip to content
  1. Docs
  2. Integrations
  3. WhatsApp

WhatsApp astrology bot, Meta Cloud API guide

Reply to HOROSCOPE ARIES with the daily reading, or to TAROT with a drawn card, on WhatsApp using the official Cloud API. One Meta access token, one RoxyAPI key, one webhook handler.

The WhatsApp Cloud API is the hosted way Meta lets a server send and receive WhatsApp messages. Unlike Telegram there is no polling: messages arrive as webhook POSTs and replies are POSTs back to a Graph API endpoint. RoxyAPI supplies the answers: 258+ endpoints across 18+ domains on one key, and a Remote MCP server per domain when you want a model to choose the call itself.

Meta onboarding is the slow part

Everything on this page works on a test business phone number and a temporary token in minutes. Going live is a separate track: a real phone number on a Meta Business portfolio, business verification, and approved message templates for anything you send outside the 24 hour customer service window. Plan for it before you promise a launch date.

Pick the path first

Two shapes, and the choice decides everything below.

You wantBuildRoxyAPI path
HOROSCOPE ARIES returns the same reading every timeA keyword botOne endpoint per keyword, called from your handler
A chat that answers "what does my chart say about my career?"An AI botRemote MCP, one URL, tools discovered at runtime

A keyword bot is cheaper, deterministic, and needs no model. An AI bot does not need you to wire endpoints at all: point the model at https://roxyapi.com/mcp/{domain} and it reads the tool catalog itself. Start at the keyword bot below, then add the MCP path when free text arrives.

Step 1: prove the call

Before Meta is involved at all, confirm your key works. Get one on the pricing page, then:

curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
  -H "X-API-Key: $ROXY_API_KEY"

A 200 returns clean JSON with sign, date, overview, love, career, luckyNumber, moonSign, moonPhase, and energyRating, plus column, the whole reading as one ready-to-send block of 120 to 180 words. Errors come back as { error, code }. Nothing is wrapped in a success envelope.

Step 2: set up the Meta app and get a lasting token

Follow the Cloud API get started guide in the App Dashboard: create an app with the Connect with customers through WhatsApp use case, connect it to a WhatsApp Business account, then open API Setup to pick a From test number, add the To number you will test with, and generate a token.

That first token expires fast

The token the API Setup panel generates is temporary and is not usable for development past the same session. Create a system user in Business Settings, assign it your app and your WhatsApp Business account with full control, and generate a token carrying business_management, whatsapp_business_messaging, and whatsapp_business_management. That is the token every example below uses.

Keep the phone number ID and the WhatsApp Business account ID from the API Setup panel; you need both. Then point Meta at your handler under Configuration, which is where the webhook URL and the verify token go.

Step 3: answer the webhook handshake

Your handler needs two routes on one path. Meta verifies the endpoint once with a GET, then only ever sends POSTs.

Meta sends hub.mode=subscribe, hub.verify_token=YOUR_SECRET, and hub.challenge=RANDOM_STRING. Check the token matches the value you typed in the Configuration panel, then echo the challenge as plain text with a 200.

curl "https://your-app.example.com/webhook?hub.mode=subscribe&hub.verify_token=my_secret&hub.challenge=12345"

A body of 12345 and a 200 verifies the endpoint.

Step 4: the keyword handler

HOROSCOPE <SIGN> in, the daily reading out.

// npm install express @roxyapi/sdk
import express from 'express';
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);
const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN!;
const ACCESS_TOKEN = process.env.WHATSAPP_ACCESS_TOKEN!;
const PHONE_ID = process.env.WHATSAPP_PHONE_NUMBER_ID!;
const GRAPH = `https://graph.facebook.com/v23.0/${PHONE_ID}/messages`;

const app = express();
app.use(express.json());

// Meta verify handshake
app.get('/webhook', (req, res) => {
  if (req.query['hub.mode'] === 'subscribe' && req.query['hub.verify_token'] === VERIFY_TOKEN) {
    return res.status(200).send(req.query['hub.challenge']);
  }
  res.sendStatus(403);
});

// Inbound messages
app.post('/webhook', async (req, res) => {
  res.sendStatus(200); // ack first, process after

  const msg = req.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0];
  if (!msg || msg.type !== 'text') return;

  const [cmd, sign] = msg.text.body.trim().toUpperCase().split(/\s+/);
  if (cmd !== 'HOROSCOPE' || !sign) return;

  const { data, error } = await roxy.astrology.getDailyHoroscope({
    path: { sign: sign.toLowerCase() },
  });
  const reply = error
    ? `Could not read ${sign}: ${error.code}`
    : `*${data.sign} for ${data.date}*\n\n${data.column}\n\nLucky number: ${data.luckyNumber}\nMoon: ${data.moonSign} (${data.moonPhase})\nEnergy: ${data.energyRating}/10`;

  await fetch(GRAPH, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${ACCESS_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      recipient_type: 'individual',
      to: msg.from,
      type: 'text',
      text: { body: reply },
    }),
  });
});

app.listen(3000);

Send HOROSCOPE ARIES from the number you added in API Setup. The reply lands within a second.

Add a tarot card as a media message

POST /tarot/daily returns card.imageUrl, a public CDN URL, and the WhatsApp image message takes a remote link directly, so there is nothing to host. Passing seed makes the draw reproducible: the same seed on the same date always returns the same card.

Drop this inside the POST handler above; it reuses roxy, msg, GRAPH, and ACCESS_TOKEN.

const { data } = await roxy.tarot.getDailyCard({ body: { seed: msg.from } });
await fetch(GRAPH, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${ACCESS_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    messaging_product: 'whatsapp',
    recipient_type: 'individual',
    to: msg.from,
    type: 'image',
    image: {
      link: data.card.imageUrl,
      caption: `${data.card.name}${data.card.reversed ? ' (reversed)' : ''}\n\n${data.dailyMessage}`,
    },
  }),
});

In Python the same call is roxy.tarot.get_daily_card(seed=msg['from']).

Step 5: more keywords

Every keyword is the same three moves: pick the endpoint, build the parameters, format the reply. Browse everything in the API reference.

The endpoints that fit a keyword reply best:

Any chart, panchang, dasha, synastry or compatibility call needs latitude, longitude, and timezone. Resolve them from the city with GET /location/search (roxy.location.searchCities in TypeScript, roxy.location.search_cities in Python) and pass the IANA timezone straight through. The server resolves DST for the chart date, so a January birth in New York gets EST and a July birth gets EDT.

Add an AI bot on Remote MCP

A keyword bot is rigid: it answers only what you predicted. For free text, connect the model to a Remote MCP server and let it choose.

The servers run at https://roxyapi.com/mcp/{domain} over Streamable HTTP, POST only. No stdio, no Docker, no local process. Authenticate with the X-API-Key header or Authorization: Bearer. The model calls tools/list, sees the typed catalog with field descriptions and enums, picks a tool, and calls it. You wire no endpoints. In the handler you forward msg.text.body to your agent and send the answer back.

Tool discovery is free. Each tools/call counts as one request, the same as the equivalent REST call. Full setup, per-client configs, and the compact argument that cuts agent token cost are on the Remote MCP page. If you would rather have a coding agent build the whole bot, the prompts page hands it the truth sources first.

Frequently asked questions

What is the 24 hour customer service window?

When a user messages your business number they open a customer service window. For 24 hours you can reply with free-form text and media. Outside it, every outbound message has to use a pre-approved template. This is Meta policy, not a RoxyAPI limit, so design keyword flows around inbound triggers and keep pushes for templates.

How many people can I message a day?

Messaging limits count the unique user phone numbers you deliver to outside a customer service window in a moving 24 hour period, and they are set per business portfolio, shared across every number in it. A new portfolio starts at 250 and scales to 2,000, 10,000, 100,000, and eventually unlimited as delivery volume and quality rating allow. See messaging limits.

Do I have to verify X-Hub-Signature-256?

For production, yes. Meta signs every webhook POST with an HMAC SHA256 of the raw body using your app secret and sends it in the X-Hub-Signature-256 header, prefixed with sha256=. Compute yours over the raw bytes, before any JSON parsing, and reject a mismatch. Recipe in the Meta webhook guide.

What about group chats?

Groups are their own surface on the platform, with separate group management and group messaging APIs you opt into, so a keyword bot built from this page only ever sees direct messages to its business number. Whichever surface you are on, cache daily content per recipient in a short-lived store so a user asking ten times costs one RoxyAPI call.

Can the bot reply in another language?

Yes. Most domains take a lang query parameter: en, tr, de, es, hi, pt, fr, ru, zh-Hans, zh-Hant. Pass query: { lang: 'hi' } in TypeScript or lang='hi' in Python. The webhook payload carries the sender profile name, not a locale, so pick the language from a stored preference or a keyword such as HI HOROSCOPE ARIES. Machine values such as sign names stay canonical; the prose translates in place.

What happens if I reply 500 to Meta?

Meta retries immediately, then a few more times with decreasing frequency across 36 hours, then drops the event. Send the 200 first and process afterwards, as the handlers above do, and deduplicate on messages[].id.

Gotchas

  • Backend-only key. The RoxyAPI key and the Meta access token both live in your handler. Never echo either to a user and never commit one.
  • Pin the Graph API version and diary the bump. The examples use v23.0. Meta keeps each version usable for at least two years after its successor ships, then falls back to the next oldest, so check the version in your URL against the versioning guide once a year.
  • The first access token expires. Use a system user token with the three WhatsApp permissions, not the one the API Setup panel hands you.
  • HTTPS with a real certificate. Self-signed will not verify. Use ngrok for local work.
  • Meta nests the message deeply. Destructure defensively through entry[0].changes[0].value.messages[0] and skip anything that is not what you expect.
  • type: 'text' is one of many. Image, audio, video, location, contacts and interactive replies all arrive in the same messages[] array, and status callbacks arrive with no messages key at all. Filter before parsing.
  • Templates need approval. A "your daily horoscope is ready" push is a template and has to be approved. A reply inside the 24 hour window is not.
  • Prefer the IANA timezone. "Europe/London" beats 5.5, because the server resolves DST for the chart date.
  • Tool discovery is free, tool calls are not. Debugging an agent against tools/list costs nothing; every tools/call is one request.

What to build next