# Build a Slack astrology bot with RoxyAPI

> Add `/horoscope`, `/tarot`, and `/lifepath` slash commands to any [Slack](https://slack.com/) workspace in under 20 minutes. Bolt SDK, Block Kit replies, RoxyAPI on the backend.

Slack is the workspace messaging platform with the most developer-friendly bot surface: [slash commands](https://api.slack.com/interactivity/slash-commands), the [Events API](https://api.slack.com/apis/connections/events-api), and the official [Bolt SDK](https://slack.dev/bolt-js) for Node.js and Python. RoxyAPI provides the spiritual data layer: 130+ astrology, Vedic astrology, tarot, and numerology endpoints behind one key, plus a remote MCP server per domain for AI-routed bots.

**TL;DR:**
- You will ship a Slack bot that responds to `/horoscope aries`, `/tarot`, and `/lifepath 1990-05-15` with rich Block Kit replies
- You need a Slack app at [api.slack.com/apps](https://api.slack.com/apps) (free) with `commands` and `chat:write` scopes, plus a [RoxyAPI key](/pricing). Plans start at $39 a month for 25K requests; a free test key is available on request via [contact](/contact)
- Working code in three languages, ready to deploy. About 15 minutes start to finish

## What you can build on Slack

- A workspace `/horoscope` slash command that posts the daily reading as a Block Kit card
- A `/tarot` command that draws a daily card and embeds the card image in the reply
- A `/lifepath <YYYY-MM-DD>` numerology command for onboarding rituals (new hire shoutouts, anniversaries)
- A scheduled morning post that drops the daily moon phase or panchang into a `#standup` channel
- An interactive `/synastry` command that asks for two members and returns a compatibility score
- An AI agent bot where the LLM picks between horoscope, kundli, and tarot endpoints based on the message
- A workspace assistant that uses Block Kit context blocks to surface lucky number, moon sign, and energy rating

## What you need, 30 seconds

1. A Slack workspace where you can install apps. Free.
2. A Slack app created at [api.slack.com/apps](https://api.slack.com/apps) with three pieces wired up: a slash command, a Bot Token (`xoxb-...`), and a Signing Secret. The [Bolt JS getting-started guide](https://tools.slack.dev/bolt-js/getting-started) and [Bolt Python getting-started guide](https://tools.slack.dev/bolt-python/getting-started) walk through the console step by step.
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 or Slack Socket Mode for local dev.
5. About 15 minutes.

**Tip: Run the [quickstart](/docs/quickstart) curl in a terminal first. A 200 response confirms the Roxy key is good before Slack tries to deliver any slash command.**

## Step 1, register the slash command and pick a transport

In the Slack app console, open **Slash Commands**, click **Create New Command**:

- **Command** `/horoscope`
- **Request URL** `https://your-app.example.com/slack/events` (the Bolt default)
- **Short description** "Get the daily horoscope"
- **Usage hint** `[zodiac sign]`

Add the `commands` and `chat:write` OAuth scopes, install the app to your workspace, copy the Bot User OAuth Token and Signing Secret. Slack offers two transports for delivering events to your handler.


### HTTP webhook (production)

Slack POSTs every slash command and event to your Request URL. Bolt verifies the [signing secret](https://api.slack.com/authentication/verifying-requests-from-slack) on every request automatically.

```bash
# Slack will fire something like this
curl -X POST "https://your-app.example.com/slack/events" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'command=/horoscope&text=aries&user_id=U123&channel_id=C456'
```

Your endpoint must be HTTPS. Use [ngrok](https://ngrok.com/) for local dev. The whole verify-and-route dance is handled by Bolt; you just register the command handler.

### Socket Mode (no public URL needed)

Bolt opens a WebSocket back to Slack and receives events over that. No HTTPS host, no ngrok, no signing-secret verification (the WebSocket itself is authenticated). Toggle Socket Mode in the app console and generate an App-Level Token (`xapp-...`) with `connections:write`.

```typescript
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
});
```

Use Socket Mode for prototyping and internal-only bots. Switch to HTTP webhook for distributed apps.


## Step 2, ship the /horoscope command

The handler reads the zodiac sign from the slash command text, calls Roxy, and replies with a Block Kit card. The example uses HTTP transport, but the body of the handler is identical under Socket Mode.


### 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`. Map them into Block Kit blocks below.

### Node.js (Bolt + SDK)

```typescript
// npm install @slack/bolt @roxyapi/sdk
import { App } from '@slack/bolt';
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});

app.command('/horoscope', async ({ command, ack, respond }) => {
  await ack();
  const sign = command.text.trim().toLowerCase();
  if (!sign) {
    return respond({ text: 'Usage: `/horoscope aries`' });
  }

  const { data, error } = await roxy.astrology.getDailyHoroscope({
    path: { sign },
  });
  if (error) {
    return respond({ text: `Could not read ${sign}: ${error.code}` });
  }

  await respond({
    blocks: [
      {
        type: 'header',
        text: { type: 'plain_text', text: `${data.sign} for ${data.date}` },
      },
      {
        type: 'section',
        text: { type: 'mrkdwn', text: data.overview },
      },
      {
        type: 'context',
        elements: [
          {
            type: 'mrkdwn',
            text: `Lucky number *${data.luckyNumber}*  ·  Moon in ${data.moonSign} (${data.moonPhase})  ·  Energy ${data.energyRating}/10`,
          },
        ],
      },
    ],
  });
});

(async () => {
  await app.start(3000);
})();
```

### Python (Bolt + SDK)

```python
# pip install slack-bolt roxy-sdk
import os
from slack_bolt import App
from roxy_sdk import create_roxy, RoxyAPIError

roxy = create_roxy(os.environ['ROXY_API_KEY'])
app = App(
    token=os.environ['SLACK_BOT_TOKEN'],
    signing_secret=os.environ['SLACK_SIGNING_SECRET'],
)

@app.command('/horoscope')
def handle_horoscope(ack, command, respond):
    ack()
    sign = command.get('text', '').strip().lower()
    if not sign:
        respond({'text': 'Usage: `/horoscope aries`'})
        return

    try:
        data = roxy.astrology.get_daily_horoscope(sign=sign)
    except RoxyAPIError as e:
        respond({'text': f'Could not read {sign}: {e.code}'})
        return

    respond(blocks=[
        {
            'type': 'header',
            'text': {'type': 'plain_text', 'text': f"{data['sign']} for {data['date']}"},
        },
        {
            'type': 'section',
            'text': {'type': 'mrkdwn', 'text': data['overview']},
        },
        {
            'type': 'context',
            'elements': [{
                'type': 'mrkdwn',
                'text': (
                    f"Lucky number *{data['luckyNumber']}*  ·  "
                    f"Moon in {data['moonSign']} ({data['moonPhase']})  ·  "
                    f"Energy {data['energyRating']}/10"
                ),
            }],
        },
    ])

if __name__ == '__main__':
    app.start(port=3000)
```


Type `/horoscope aries` in any channel where the app is installed. The reply lands inline as a richly formatted card.

### Bonus, a /tarot command with the card image

The `tarot/daily` response carries `card.imageUrl`, a public CDN URL. The Block Kit `image` block accepts a remote URL directly, so you can return the card image with no extra hosting.

```typescript
app.command('/tarot', async ({ command, ack, respond }) => {
  await ack();
  const { data } = await roxy.tarot.getDailyCard({
    body: { seed: command.user_id }, // deterministic per user per day
  });
  await respond({
    blocks: [
      {
        type: 'image',
        image_url: data.card.imageUrl,
        alt_text: data.card.name,
        title: {
          type: 'plain_text',
          text: `${data.card.name}${data.card.reversed ? ' (reversed)' : ''}`,
        },
      },
      { type: 'section', text: { type: 'mrkdwn', text: data.dailyMessage } },
    ],
  });
});
```

Seeding by `command.user_id` makes the daily card deterministic per user per day. Same Slack member calls `/tarot` ten times before midnight, gets the same card every time. Add the `files:write` scope if you want to upload images directly instead of linking the CDN URL.

## Step 3, scale to the full surface

Adding a new slash command is the same shape as `/horoscope`. Pick the endpoint, build the parameters, format the Block Kit response.

- **[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 Slack slash replies:
  - [`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 /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
  - [`POST /biorhythm/daily`](/api-reference#tag/biorhythm/POST/biorhythm/daily) as a wellness add-on for productivity workspaces

Any chart, panchang, dasha, dosha, 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)

For free-text questions in DMs ("what does my chart say about my career?"), 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. Inside a Bolt `app.message` handler, forward the message text to the agent, await the answer, post it back via `say()` or `respond()`.

`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 Bolt verify signing secrets automatically?
Slack signs every webhook POST with HMAC SHA256. Bolt checks the `X-Slack-Signature` and `X-Slack-Request-Timestamp` headers on every request and rejects mismatches before your handler runs. You only set `signingSecret` in the App constructor; the rest is handled.

### Can I call ack() after a long Roxy call?
No. Slack expects an ack within 3 seconds. Always call `await ack()` first, then call Roxy, then `respond()` (which posts via the `response_url` Slack passes in the slash command payload). The example handlers above follow this pattern.

### How do I keep the bot from leaking horoscopes to the wrong channel?
By default `respond()` replies in the channel where the slash command was issued. To DM a user instead, switch to `client.chat.postMessage({ channel: command.user_id, ... })` (Slack accepts user IDs as DM channels) or set the slash command response_type to `ephemeral` so only the invoking user sees it.

### 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). Detect the user language from the workspace locale or a per-user preference.

### What scopes do I need beyond `commands`?
`chat:write` to post messages back. Add `files:write` only if you want to upload images directly (linking the `imageUrl` CDN URL in a Block Kit `image` block does not need it). For DMs, add `im:write`.

## Gotchas

- **Backend-only key.** The Roxy key and Slack tokens both live in your handler. Never echo either to a channel, never check them into git.
- **3-second ack budget.** Always `await ack()` first, then call Roxy. The slash command will time out if you hold the ack for the API call.
- **`response_url` is good for 30 minutes.** `respond()` uses it under the hood. After 30 minutes, post via `client.chat.postMessage` instead.
- **Rate limits are tier-based.** Most workspaces sit at Tier 3 (50 calls per minute on `chat.postMessage`). Bolt does not throttle for you, so heavy bulk sends need explicit pacing.
- **Block Kit text fields cap at 3000 chars.** The Roxy `overview` field is short, but `interpretation.detailed` from a natal chart can be longer. Truncate or split into multiple section blocks.
- **Slack mrkdwn is not Markdown.** Bold is `*text*` (single asterisks), italic is `_text_`. Code is single backticks. No real headers (use a `header` block instead).
- **Timezone IANA wins.** Prefer `"Europe/London"` to `5.5`. The server resolves DST per chart date.
- **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 [WhatsApp integration](/docs/integrations/whatsapp) 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.
