# Build a WhatsApp astrology bot with RoxyAPI

> Reply to "HOROSCOPE ARIES" with the daily reading, or to "KUNDLI" with a Vedic chart, on [WhatsApp](https://www.whatsapp.com/) using the official [Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api). One Meta access token, one Roxy API key, one webhook handler.

The WhatsApp [Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started) 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 ARIES` and `TAROT` with live RoxyAPI data
- You need a [Meta Developer account](https://developers.facebook.com/) (free), a WhatsApp Cloud API test number (free during development), and a [RoxyAPI key](/pricing) (plans start at $39 a month, free test key on request via [contact](/contact))
- Working code in three languages, ready to deploy to any HTTPS host. About 30 minutes start to finish, most of it Meta onboarding

**Warning: 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](https://developers.facebook.com/docs/whatsapp/business-management-api/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 LEO` with the daily reading
- A `KUNDLI` flow 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 LONDON`)
- 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

1. A [Meta for Developers](https://developers.facebook.com/) account. Free.
2. A WhatsApp app set up in [Meta Developer Console](https://developers.facebook.com/apps), with a test phone number and access token. Follow the [Cloud API getting-started guide](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started). Free during development.
3. A RoxyAPI key. Plans start at $39 a month on the [pricing page](/pricing); a free test key for evaluation is available on request via [contact](/contact).
4. A public HTTPS endpoint for the webhook. [ngrok](https://ngrok.com/) works for local testing. Vercel, Fly.io, Railway, Render, or any host with TLS works for production.
5. About 30 minutes, most of it Meta onboarding.

**Tip: Run the [quickstart](/docs/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.


### GET verify handshake

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.

```bash
# 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.

### POST message receive

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:

```json
{
  "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 (smoke test the endpoint)

```bash
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.

### Node.js (Express + SDK)

```typescript
// 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);
```

### Python (FastAPI + SDK)

```python
# 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.

```typescript
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](/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](/docs/guides/astrology), [Vedic astrology](/docs/guides/vedic-astrology), [Tarot](/docs/guides/tarot), [Numerology](/docs/guides/numerology)
- **Most-used endpoints** that fit WhatsApp keyword bots:
  - [`GET /astrology/horoscope/{sign}/daily`](/api-reference#tag/western-astrology/GET/astrology/horoscope/{sign}/daily) for daily, weekly, monthly variants
  - [`POST /vedic-astrology/birth-chart`](/api-reference#tag/vedic-astrology/POST/vedic-astrology/birth-chart) for kundli (call [`GET /location/search`](/api-reference#tag/location-and-timezone/GET/location/search) first to geocode the city)
  - [`POST /vedic-astrology/panchang/detailed`](/api-reference#tag/vedic-astrology/POST/vedic-astrology/panchang/detailed) for daily Hindu calendar lookups
  - [`POST /numerology/life-path`](/api-reference#tag/numerology/POST/numerology/life-path) for the Life Path number
  - [`POST /tarot/spreads/three-card`](/api-reference#tag/tarot/POST/tarot/spreads/three-card) for 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](/docs/guides/claude-code), [Cursor](/docs/guides/cursor), [OpenAI Agents SDK](/docs/guides/function-calling), 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](/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](https://www.facebook.com/business/help/2058515294227817), and approved [message templates](https://developers.facebook.com/docs/whatsapp/business-management-api/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](https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verify-payloads).

### 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](https://ngrok.com/) 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 same `messages[]` array. Filter on `msg.type` before 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](https://developers.facebook.com/docs/whatsapp/business-management-api/message-templates). Reactive replies inside the 24-hour window do not.
- **Timezone IANA wins.** Prefer `"Europe/London"` to `5.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/call` is billable, `tools/list` is free.** When debugging an agent, the discovery call does not count, the actual answer does.

## What to build next

- The [Western astrology guide](/docs/guides/astrology) and [Vedic astrology guide](/docs/guides/vedic-astrology) cover endpoint ordering for chart and kundli flows.
- The [Telegram integration](/docs/integrations/telegram) and [Slack integration](/docs/integrations/slack) cover the same patterns on adjacent messaging platforms.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) is the reference for wiring multiple domains into an LLM-routed agent.
- Browse the [API reference](/api-reference) for every endpoint across 12 domains.
