- Docs
- Integrations
- Slack
Slack astrology bot, slash commands and tarot
Ship
/horoscope,/tarot, and/lifepathin a Slack workspace in about 15 minutes. Paste one app manifest, add one handler, reply with Block Kit.
Slack gives a bot three things to build on: slash commands, the Events API, and the official Bolt SDK for Node.js and Python. 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.
Pick the path first
Two shapes, and the choice decides everything below.
| You want | Build | RoxyAPI path |
|---|---|---|
/horoscope aries returns the same card every time | A command bot | One endpoint per command, called from your handler |
| A DM that answers "what does my chart say about my career?" | An AI bot | Remote MCP, one URL, tools discovered at runtime |
A command 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 command bot below, then add the MCP path when free text arrives.
Step 1: prove the call
Before Slack 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-post block of 120 to 180 words. Errors come back as { error, code }. Nothing is wrapped in a success envelope.
column is the fastest path to a good-looking Slack reply: one field, already written as prose, well inside the 3000 character cap on a Block Kit section. Use the six topic fields (overview, love, career, health, finance, advice) only when you want a block per life area. They read the same dated events as column, each written as a column of its own, so render one shape or the other, never both.
Step 2: create the Slack app from a manifest
Manifests beat clicking through panels: one paste sets the command, the scopes, and the request URL together. At api.slack.com/apps, choose Create New App, then From a manifest, pick the workspace, and paste this. Replace the domain with your own.
{
"display_information": { "name": "Roxy" },
"features": {
"bot_user": { "display_name": "Roxy", "always_online": false },
"slash_commands": [
{
"command": "/horoscope",
"url": "https://your-app.example.com/slack/events",
"description": "Get the daily horoscope",
"usage_hint": "[zodiac sign]",
"should_escape": false
},
{
"command": "/tarot",
"url": "https://your-app.example.com/slack/events",
"description": "Draw the daily tarot card",
"should_escape": false
}
]
},
"oauth_config": { "scopes": { "bot": ["commands", "chat:write"] } },
"settings": {
"interactivity": { "is_enabled": true },
"org_deploy_enabled": false,
"socket_mode_enabled": false,
"token_rotation_enabled": false
}
}
Install to the workspace, then copy the Bot User OAuth Token (xoxb-) and the Signing Secret from Basic Information. Both go in your environment, never in a repo.
Slack POSTs every slash command to the url in the manifest. Bolt verifies the signing secret on every request before your handler runs, so you set signingSecret and write no crypto. The endpoint must be HTTPS; use ngrok locally.
Bolt opens a WebSocket to Slack instead, so there is no request URL to expose. Set socket_mode_enabled to true in the manifest, generate an App-Level Token (xapp-) with connections:write under Basic Information, and drop the url from the slash command.
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
socketMode: true,
appToken: process.env.SLACK_APP_TOKEN,
});
Good for prototypes and internal bots. Switch to the HTTP webhook for anything distributed.
Step 3: the /horoscope handler
Acknowledge inside 3 seconds, then call RoxyAPI, then reply. The handler body is identical under either transport.
// 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({
response_type: 'in_channel',
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: `${data.sign} for ${data.date}` },
},
{
type: 'section',
text: { type: 'mrkdwn', text: data.column },
},
{
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);
})();
# 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(
response_type='in_channel',
blocks=[
{
'type': 'header',
'text': {'type': 'plain_text', 'text': f"{data['sign']} for {data['date']}"},
},
{
'type': 'section',
'text': {'type': 'mrkdwn', 'text': data['column']},
},
{
'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 the app is installed in. Drop response_type and the reply is ephemeral instead, visible only to whoever ran the command.
Add /tarot with the card image
POST /tarot/daily returns card.imageUrl, a public CDN URL, so a Block Kit image block can point straight at it with nothing to host. Passing seed makes the draw reproducible: the same seed on the same date always returns the same card, so a member who runs /tarot ten times before midnight sees one card.
app.command('/tarot', async ({ command, ack, respond }) => {
await ack();
const { data } = await roxy.tarot.getDailyCard({
body: { seed: command.user_id },
});
await respond({
response_type: 'in_channel',
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 } },
],
});
});
Add the files:write scope only if you would rather upload the image than link it.
Step 4: more commands
Every command is the same three moves: pick the endpoint, build the parameters, format the blocks. Browse everything in the API reference.
- Domain guides carry the endpoint ordering per domain: Western Astrology, Vedic Astrology, KP Astrology, Forecast, Human Design, Chinese Astrology, Feng Shui, Mesoamerican Astrology, Vastu, Numerology, Kabbalah, Tarot, Biorhythm, Ayurveda, I-Ching, Crystals, Dreams, Angel Numbers
The endpoints that fit a slash reply best:
GET /astrology/horoscope/{sign}/daily, plus weekly, monthly and yearly on the same shapePOST /tarot/spreads/three-cardfor past, present, futurePOST /numerology/life-pathfromyear,month,day, good for onboarding ritualsPOST /astrology/compatibility-scorefor a two-member/synastrycommandPOST /vedic-astrology/birth-chartfor a kundliPOST /biorhythm/dailyas a wellness add-on
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
Hardcoded commands stop at the questions you predicted. For free text in DMs, 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. Inside a Bolt app.message handler, forward the text to your agent and post the answer back with say().
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
Does the reply go to the channel or just to me?
Just to you, unless you say otherwise. A response_url reply is ephemeral by default, so only the member who ran the command sees it. Set response_type: 'in_channel' in the payload, as the handlers above do, to post it for everyone. Declare it either way rather than relying on the default.
Can I call ack() after the RoxyAPI call?
No. Slack wants the acknowledgement within 3000 milliseconds or the member sees an operation_timeout error. Call ack() first, then RoxyAPI, then respond(), which posts through the response_url Slack sent with the payload. Bolt for Python also has lazy listeners for work that runs long.
How long is response_url good for?
Up to 5 responses within 30 minutes of the payload. After that, post with client.chat.postMessage instead.
How do I DM a member instead?
client.chat.postMessage({ channel: command.user_id, ... }). Slack accepts a user ID as a DM channel. Add the im:write scope.
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. Machine values such as sign names stay canonical; the prose translates in place.
What scopes do I need beyond commands?
chat:write to reply. im:write for DMs. files:write only if you upload images rather than linking a URL in an image block.
Gotchas
- Backend-only key. The RoxyAPI key, the bot token, and the signing secret all stay in your handler. Never echo one into a channel and never commit one.
- 3 second acknowledgement. Always
await ack()before the API call, never after. - Posting is rate limited per channel. Slack expects no more than one message per second per channel and allows short bursts above it. Bolt does not throttle for you, so a bulk send needs explicit pacing.
- Block Kit has hard caps. A section
textmaxes at 3000 characters and an imagetitleat 2000. A dailycolumnfits comfortably; a full natal chart interpretation does not, so truncate or split across sections. - Slack mrkdwn is not Markdown. Bold is
*text*with single asterisks, italic is_text_. There are no headings, so use aheaderblock. - Prefer the IANA timezone.
"Europe/London"beats5.5, because the server resolves DST for the chart date. - Tool discovery is free, tool calls are not. Debugging an agent against
tools/listcosts nothing; everytools/callis one request.
What to build next
- Western astrology and Vedic astrology cover endpoint ordering for chart and kundli flows.
- Telegram and WhatsApp are the same patterns on adjacent platforms.
- The AI chatbot tutorial wires several domains into one LLM-routed agent.
- The API reference has every endpoint across 18+ domains.