- Docs
- Integrations
Build a WhatsApp astrology bot with RoxyAPI
Reply to "HOROSCOPE ARIES" with the daily reading, or to "KUNDLI" with a Vedic chart, on WhatsApp using the official Cloud API. One Meta access token, one Roxy API key, one webhook handler.
The WhatsApp Cloud API is the hosted, no-infrastructure way Meta lets you send and receive WhatsApp messages from a server. Unlike Telegram, WhatsApp is a webhook-only platform: messages arrive as POST requests, replies are POST requests back to a Meta endpoint. RoxyAPI provides the spiritual data layer behind your bot: 130+ astrology, Vedic astrology, tarot, and numerology endpoints behind one key, plus a remote MCP server per domain when you want an LLM to route the call on its own.
TL;DR
- You will ship a WhatsApp bot that replies to keywords like
HOROSCOPE ARIESandTAROTwith live RoxyAPI data - You need a Meta Developer account (free), a WhatsApp Cloud API test number (free during development), and a RoxyAPI key (plans start at $39 a month, free test key on request via contact)
- Working code in three languages, ready to deploy to any HTTPS host. About 30 minutes start to finish, most of it Meta onboarding
WhatsApp Cloud API has stricter onboarding than Telegram. The free test number can message up to 5 verified recipients during development. Production needs a real phone number tied to a Meta Business account, business verification, and approved message templates for any outbound message sent outside the 24-hour customer-service window. Plan for that before you scale.
What you can build on WhatsApp
- A keyword-triggered horoscope bot that replies to
HOROSCOPE LEOwith the daily reading - A
KUNDLIflow that asks for birth date, time, and city, then replies with a Vedic chart - A daily tarot card pushed to opted-in subscribers via approved message templates
- A multi-language wellness bot that picks language from the WhatsApp user locale and calls Roxy with
?lang=hi,?lang=es, or?lang=tr - A panchang assistant for Hindu calendar lookups (
PANCHANG TODAY MUMBAI) - A numerology onboarding flow that returns Life Path, Expression, and Soul Urge from a name and birth date
- An AI agent bot where the LLM picks between horoscope, kundli, and tarot endpoints based on the message text
What you need
- A Meta for Developers account. Free.
- A WhatsApp app set up in Meta Developer Console, with a test phone number and access token. Follow the Cloud API getting-started guide. Free during development.
- A RoxyAPI key. Plans start at $39 a month on the pricing page; a free test key for evaluation is available on request via contact.
- A public HTTPS endpoint for the webhook. ngrok works for local testing. Vercel, Fly.io, Railway, Render, or any host with TLS works for production.
- About 30 minutes, most of it Meta onboarding.
Run the quickstart curl in a terminal first. A 200 response confirms the Roxy key is good before Meta tries to POST anything to your handler.
Step 1, register the webhook with Meta
Meta verifies your webhook endpoint with a one-time GET handshake before subscribing it to message events. Your handler needs two routes on the same path: a GET that echoes the verify challenge, and a POST that processes incoming messages.
Meta sends a one-time GET with three query params: hub.mode=subscribe, hub.verify_token=YOUR_SECRET, and hub.challenge=RANDOM_STRING. Your handler checks the token matches a value you set in the Developer Console, and echoes the challenge as plain text.
# Meta will fire something like this against your endpoint
curl "https://your-app.example.com/webhook?hub.mode=subscribe&hub.verify_token=my_secret&hub.challenge=12345"
If the response is 12345 with status 200, the webhook is verified. The token never appears again. From this point on Meta only sends POSTs.
Every inbound WhatsApp message arrives as a JSON POST with a deeply nested entry[].changes[].value.messages[] shape. The fields you care about for a basic command bot:
{
"entry": [{
"changes": [{
"value": {
"metadata": { "phone_number_id": "..." },
"messages": [{
"from": "15551234567",
"text": { "body": "HOROSCOPE ARIES" },
"type": "text"
}]
}
}]
}]
}
Meta retries delivery for up to 7 days if your endpoint returns non-2xx, so always reply 200 fast and process the message asynchronously if it might take more than a second.
Step 2, ship the keyword reply handler
The example below handles a one-shot HOROSCOPE <SIGN> keyword and replies with the daily reading. Run the curl smoke test first to confirm Roxy is responding, then drop the handler behind your webhook URL.
curl "https://roxyapi.com/api/v2/astrology/horoscope/aries/daily" \
-H "X-API-Key: $ROXY_API_KEY"
You should get back JSON with sign, date, overview, love, career, luckyNumber, moonSign, moonPhase, and energyRating. Pick the fields you want in the bot reply.
// 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/v21.0/${PHONE_ID}/messages`;
const app = express();
app.use(express.json());
// Meta verify handshake
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
return res.status(200).send(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 text = msg.text.body.trim().toUpperCase();
const [cmd, sign] = text.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.overview}\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',
to: msg.from,
text: { body: reply },
}),
});
});
app.listen(3000);
# pip install fastapi uvicorn httpx roxy-sdk
import os
import httpx
from fastapi import FastAPI, Request, Response, HTTPException
from roxy_sdk import create_roxy, RoxyAPIError
roxy = create_roxy(os.environ['ROXY_API_KEY'])
VERIFY_TOKEN = os.environ['WHATSAPP_VERIFY_TOKEN']
ACCESS_TOKEN = os.environ['WHATSAPP_ACCESS_TOKEN']
PHONE_ID = os.environ['WHATSAPP_PHONE_NUMBER_ID']
GRAPH = f"https://graph.facebook.com/v21.0/{PHONE_ID}/messages"
app = FastAPI()
@app.get('/webhook')
def verify(request: Request):
params = request.query_params
if params.get('hub.mode') == 'subscribe' and params.get('hub.verify_token') == VERIFY_TOKEN:
return Response(content=params.get('hub.challenge', ''), media_type='text/plain')
raise HTTPException(status_code=403)
@app.post('/webhook')
async def receive(request: Request):
body = await request.json()
try:
msg = body['entry'][0]['changes'][0]['value']['messages'][0]
except (KeyError, IndexError):
return {'ok': True}
if msg.get('type') != 'text':
return {'ok': True}
text = msg['text']['body'].strip().upper()
parts = text.split()
if len(parts) < 2 or parts[0] != 'HOROSCOPE':
return {'ok': True}
sign = parts[1].lower()
try:
data = roxy.astrology.get_daily_horoscope(sign=sign)
reply = (
f"*{data['sign']} for {data['date']}*\n\n"
f"{data['overview']}\n\n"
f"Lucky number: {data['luckyNumber']}\n"
f"Moon: {data['moonSign']} ({data['moonPhase']})\n"
f"Energy: {data['energyRating']}/10"
)
except RoxyAPIError as e:
reply = f"Could not read {sign}: {e.code}"
async with httpx.AsyncClient() as client:
await client.post(
GRAPH,
headers={'Authorization': f'Bearer {ACCESS_TOKEN}'},
json={
'messaging_product': 'whatsapp',
'to': msg['from'],
'text': {'body': reply},
},
)
return {'ok': True}
# uvicorn whatsapp_bot:app --reload --port 3000
Send HOROSCOPE ARIES from a verified test recipient to your WhatsApp test number. The bot replies within a second.
Bonus, send the daily tarot card as a media message
The tarot/daily response carries card.imageUrl, a public CDN URL. WhatsApp accepts a remote image URL in the image payload, so you can return the card image with no extra hosting.
Drop this block inside the /webhook POST handler from above; it reuses the same roxy, msg, GRAPH, and ACCESS_TOKEN variables.
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',
to: msg.from,
type: 'image',
image: {
link: data.card.imageUrl,
caption: `${data.card.name}${data.card.reversed ? ' (reversed)' : ''}\n\n${data.dailyMessage}`,
},
}),
});
Seeding by msg.from (the user phone number) makes the daily card deterministic per user per day. Same recipient asks ten times before midnight, gets the same card every time. Works the same way in Python via roxy.tarot.get_daily_card(seed=msg['from']).
Step 3, scale to the full surface
Adding a new keyword is the same shape as HOROSCOPE. Pick the endpoint, build the parameters, format the reply.
- API reference with a pre-filled test key. Browse endpoints, run a call in the browser, copy the curl
- Domain guides for endpoint ordering: Western astrology, Vedic astrology, Tarot, Numerology
- Most-used endpoints that fit WhatsApp keyword bots:
GET /astrology/horoscope/{sign}/dailyfor daily, weekly, monthly variantsPOST /vedic-astrology/birth-chartfor kundli (callGET /location/searchfirst to geocode the city)POST /vedic-astrology/panchang/detailedfor daily Hindu calendar lookupsPOST /numerology/life-pathfor the Life Path numberPOST /tarot/spreads/three-cardfor past, present, future spreads
Any chart, panchang, dasha, dosha, navamsa, KP, synastry, or compatibility endpoint needs latitude, longitude, and timezone. Always call roxy.location.searchCities (TS) or roxy.location.search_cities (Py) first. Pass the IANA timezone straight through; the server resolves DST for the chart date.
Add an MCP-powered AI agent (optional)
A keyword bot is rigid. For free-text questions ("what does my chart say about marriage?"), point an LLM at the Roxy MCP server and let it pick the endpoint.
The remote MCP server runs at https://roxyapi.com/mcp/{domain} over Streamable HTTP. No stdio, no Docker, no local setup. Point Claude Code, Cursor, OpenAI Agents SDK, or any other MCP-aware client at the URL with your X-API-Key header. The agent inspects tools/list, picks the right tool, builds parameters, and calls Roxy.
Inside your WhatsApp handler, forward msg.text.body to the agent, await the answer, send it back. tools/list is free. Every tools/call bills the same as the equivalent REST call. See /docs/mcp for the full setup.
Frequently asked questions
Why does Meta only let me message 5 numbers in development?
The free test number is sandboxed. Add up to 5 phone numbers as verified recipients in the WhatsApp app settings. To message anyone outside the list, you need a real phone number, Meta Business verification, and approved message templates.
What is the 24-hour customer-service window?
Once a user messages your bot, you have 24 hours to reply with any free-form text or media. Outside that window, every outbound message must use a pre-approved template. This is a Meta policy, not a Roxy limit. Plan keyword and reply flows around inbound triggers.
How do I keep the bot from burning quota on group spam?
WhatsApp Cloud API does not deliver group messages to bots. You only see direct messages to your business number, so group spam is not a vector. Standard rate limits still apply: cache daily content (horoscopes, panchang) per recipient with a short-lived KV so repeat senders pay for one Roxy call.
Can the bot reply in Hindi, Spanish, or Turkish?
Yes. Most Roxy domains accept a lang query parameter (en, tr, de, es, fr, hi, pt, ru). In the SDK, pass query: { lang: 'hi' } (TS) or lang='hi' (Py). Meta also passes contacts[0].profile.locale in some payloads if you want to detect language from the user.
Should I verify the X-Hub-Signature-256 header?
Yes for production. Meta signs every webhook POST with HMAC SHA256 using your app secret. Compare the X-Hub-Signature-256 header against sha256=<hmac-of-raw-body>. Reject mismatches. The exact recipe is in the Meta webhook security guide.
What happens if I reply 500 to Meta?
Meta retries delivery for up to 7 days. Always send a 200 first, then process the message asynchronously. The example handlers above ack inside the route and process the Roxy call after.
Gotchas
- Backend-only key. The Roxy key and Meta access token both live in your handler. Never echo either to a WhatsApp user, never check them into git.
- Webhook URL must be HTTPS with a valid cert. Self-signed certs do not work. Use ngrok for local dev.
- Meta deeply nests the message. Always destructure defensively (
body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]) and skip non-text messages instead of crashing on the parse. type: 'text'is one of many. Image, audio, video, location, contacts, and interactive replies all arrive in the samemessages[]array. Filter onmsg.typebefore parsing.- Outbound rate limits are tier-based (250 to 100K conversations per 24 hours depending on your messaging tier). New numbers start at the lowest tier and graduate based on quality score.
- Templates need approval. A "Your daily horoscope is ready" push needs an approved Marketing template. Reactive replies inside the 24-hour window do not.
- Timezone IANA wins. Prefer
"Asia/Kolkata"to5.5. The server resolves DST per chart date, so a January 1990 New York birth gets EST and a July birth gets EDT automatically. - MCP
tools/callis billable,tools/listis free. When debugging an agent, the discovery call does not count, the actual answer does.
What to build next
- The Western astrology guide and Vedic astrology guide cover endpoint ordering for chart and kundli flows.
- The Telegram integration and Slack integration cover the same patterns on adjacent messaging platforms.
- The AI chatbot tutorial is the reference for wiring multiple domains into an LLM-routed agent.
- Browse the API reference for every endpoint across 10 domains.