Guides
Build an AI astrologer with tool calling
An AI astrologer is a chat assistant that reads a real birth chart and explains it in plain words. The language model (LLM) writes the words. Our API does the math. Tool calling is the link between them.
Tool calling means you describe a function to the model. The model can then ask your code to run it. Your code runs the function and sends the result back. This guide builds one tool, get_birth_chart, and runs the full loop with Claude in Python and Node.
https://json.astrologyapi.com/v1. Every call is a POST with a JSON body. Free-trial and wallet accounts send an access token in the x-astrologyapi-key header. Plan subscribers can use HTTP Basic auth instead: the user ID is the username and the API key is the password. We don't send CORS headers, so your tool function must run on your server, never in the browser.Why the model needs a tool
A language model predicts the next words. It does not run the astronomy math behind a chart, so it guesses planet degrees and rising signs. The guess often looks right and is wrong. Read Ground an LLM on computed charts for the full idea, or our blog post on why ChatGPT can't calculate birth charts.
Three ways to give an AI real chart data
You have three options. They all use our calculations. They differ in who runs the model and how much code you write.
| Option | Who runs the model | What you set up | Best when |
|---|---|---|---|
| Tool calling in your code (this guide) | You, with your own LLM provider account | A tool definition, a small loop, and a server function that calls our JSON API with your access token or API key | You want full control of the prompt, the model and your own chat screen |
| MCP server | The AI client, such as Claude Code, Claude Desktop, Cursor, VS Code or ChatGPT | A config entry with our server URL https://mcp.astrologyapi.com/mcp and your token in the x-astrologyapi-key header | You want 109 Vedic and Western tools inside an assistant you already use, with no loop code |
| Hosted Chat API | We do | One HTTP call per question from your backend, plus the session ID we return | You want a ready Kundli, Western or compatibility chat without writing any prompt |
MCP stands for Model Context Protocol. It is an open standard that lets AI clients discover and call tools on a server. Our MCP server page has the setup for each client. To give coding assistants our docs as plain text, see Build with AstrologyAPI and LLMs.
Step 1: define one tool
A tool definition has three parts. The name is what the model asks for. The description tells the model when to use it. The input_schema is a JSON Schema, a standard way to describe the fields and their types.
Our tool maps to the western_horoscope endpoint. It returns the whole Western chart in one call. The model fills in the eight birth fields. Your code adds the two fixed ones.
{
"name": "get_birth_chart",
"description": "Returns a computed Western (tropical) birth chart from AstrologyAPI: each planet's sign, degree, house and retrograde flag, the 12 house cusps (Placidus), the ascendant, the midheaven and the aspects between planets. Call this before you state any planet, sign, house or aspect. Never estimate these values yourself.",
"input_schema": {
"type": "object",
"properties": {
"day": { "type": "integer", "description": "Day of birth, 1 to 31" },
"month": { "type": "integer", "description": "Month of birth, 1 to 12" },
"year": { "type": "integer", "description": "Year of birth, e.g. 1990" },
"hour": { "type": "integer", "description": "Hour of birth, 24-hour local time, 0 to 23" },
"min": { "type": "integer", "description": "Minute of birth, 0 to 59" },
"lat": { "type": "number", "description": "Latitude of the birth place in decimal degrees, e.g. 19.2056" },
"lon": { "type": "number", "description": "Longitude of the birth place in decimal degrees, e.g. 25.2056" },
"tzone": { "type": "number", "description": "UTC offset in hours at the birth date and time, e.g. 5.5 or -5" }
},
"required": ["day", "month", "year", "hour", "min", "lat", "lon", "tzone"]
}
}The fields mean:
day,month,year: the birth date, as integers.hour,min: the local birth time, in 24-hour format.lat,lon: the birth place, in decimal degrees.tzone: the offset from UTC in hours at the birth moment, for example5.5. See Timezones, DST, and historical birth data to get it right.
Your tool function then sends this body to us. We also require house_type and is_asteroids on this endpoint, so the code sets them to placidus and false.
POST https://json.astrologyapi.com/v1/western_horoscope
{
"day": 10, "month": 5, "year": 1990,
"hour": 19, "min": 55,
"lat": 19.2056, "lon": 25.2056, "tzone": 5.5,
"house_type": "placidus",
"is_asteroids": false
}We return the chart as JSON. This is a trimmed copy of our sample response. Note that is_retro is a string, "true" or "false".
{
"planets": [
{
"name": "Moon",
"full_degree": 12.271,
"norm_degree": 12.271,
"speed": 13.5085,
"is_retro": "false",
"sign_id": 1,
"sign": "Aries",
"house": 5
}
// ...one object per planet
],
"houses": [
{ "house": 1, "sign": "Sagittarius", "degree": 240.71431 }
// ...houses 2 to 12
],
"ascendant": 240.71431015862024,
"midheaven": 156.92135925483103,
"aspects": [
{
"aspecting_planet": "Sun",
"aspected_planet": "Mercury",
"aspecting_planet_id": 0,
"aspected_planet_id": 3,
"type": "Conjunction",
"orb": 2.66,
"diff": 2.66
}
// ...more aspects
]
}The ascendant (the rising sign degree) and midheaven are longitudes from 0 to 360. An aspect is an angle between two planets, such as a conjunction. For a Vedic assistant, point the same tool at our planets endpoint. It takes the same eight fields and needs no house_type.
Step 2: run the tool-use loop with Claude
The loop has four moves. You send the question and the tool list. The model answers with text, or it asks for a tool. If it asks, you run the tool and send back a tool_result. You repeat until the model ends its turn.
stop_reasonoftool_usemeans the model wants a tool. Add its full reply to the history, then run eachtool_useblock.- Send every
tool_resultback in one user message. Each result carries the matchingtool_use_id. end_turnmeans the answer is ready. Read thetextblocks.refusalmeans the model declined. Show a polite message and stop. In production, you can also turn on Anthropic's server-side fallbacks, which retry a declined request on another model for you.
The model gives you its tool input as an object (block.input). Read fields from that object. Never parse or string-match the raw text.
Python
Install the packages with pip install anthropic requests. Set ANTHROPIC_API_KEY for Claude. For our API, set ASTROLOGY_ACCESS_TOKEN, or set ASTROLOGY_USER_ID and ASTROLOGY_API_KEY. Then run python ai_astrologer.py.
If ASTROLOGY_ACCESS_TOKEN is set, the tool sends it in the x-astrologyapi-key header. Otherwise it uses Basic auth with your user ID and API key. To see which one your account has, read Access tokens vs. User ID and API key.
# ai_astrologer.py - Python 3.9+
# pip install anthropic requests
# Run this on your server. Never put your API keys in browser code.
import json
import os
import anthropic
import requests
ASTRO_BASE = "https://json.astrologyapi.com/v1"
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
SYSTEM_PROMPT = (
"You are an astrology assistant. Always call get_birth_chart for planet, "
"sign, house and aspect positions. Never guess a position. Use only values "
"from the tool result. If the user has not given an exact birth date, time "
"and place, ask for them before you call the tool."
)
tools = [
{
"name": "get_birth_chart",
"description": (
"Returns a computed Western (tropical) birth chart from AstrologyAPI: "
"each planet's sign, degree, house and retrograde flag, the 12 house "
"cusps (Placidus), the ascendant, the midheaven and the aspects between "
"planets. Call this before you state any planet, sign, house or aspect. "
"Never estimate these values yourself."
),
"input_schema": {
"type": "object",
"properties": {
"day": {"type": "integer", "description": "Day of birth, 1 to 31"},
"month": {"type": "integer", "description": "Month of birth, 1 to 12"},
"year": {"type": "integer", "description": "Year of birth, e.g. 1990"},
"hour": {"type": "integer", "description": "Hour of birth, 24-hour local time, 0 to 23"},
"min": {"type": "integer", "description": "Minute of birth, 0 to 59"},
"lat": {"type": "number", "description": "Latitude of the birth place in decimal degrees, e.g. 19.2056"},
"lon": {"type": "number", "description": "Longitude of the birth place in decimal degrees, e.g. 25.2056"},
"tzone": {"type": "number", "description": "UTC offset in hours at the birth date and time, e.g. 5.5 or -5"},
},
"required": ["day", "month", "year", "hour", "min", "lat", "lon", "tzone"],
},
}
]
def get_birth_chart(birth):
"""Call our western_horoscope endpoint and return the JSON as a string."""
body = {
"day": birth["day"],
"month": birth["month"],
"year": birth["year"],
"hour": birth["hour"],
"min": birth["min"],
"lat": birth["lat"],
"lon": birth["lon"],
"tzone": birth["tzone"],
"house_type": "placidus",
"is_asteroids": False,
}
token = os.environ.get("ASTROLOGY_ACCESS_TOKEN")
if token:
# Free-trial and wallet accounts: access token in a header.
auth_args = {"headers": {"x-astrologyapi-key": token}}
else:
# Plan subscribers: HTTP Basic auth with user ID and API key.
auth_args = {
"auth": (os.environ["ASTROLOGY_USER_ID"], os.environ["ASTROLOGY_API_KEY"])
}
res = requests.post(
f"{ASTRO_BASE}/western_horoscope",
json=body, # JSON body either way
timeout=30,
**auth_args,
)
if res.status_code != 200:
raise RuntimeError(
f"western_horoscope failed with {res.status_code}: {res.text[:200]}"
)
return json.dumps(res.json())
def run_tool(name, tool_input):
if name == "get_birth_chart":
return get_birth_chart(tool_input) # tool_input is already a dict
raise ValueError(f"Unknown tool: {name}")
def ask(question):
messages = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=16000,
system=SYSTEM_PROMPT,
tools=tools,
messages=messages,
)
if response.stop_reason == "refusal":
return "Sorry, I can't help with that request."
if response.stop_reason == "end_turn":
break
if response.stop_reason != "tool_use":
raise RuntimeError(f"Model stopped early: {response.stop_reason}")
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
try:
content = run_tool(block.name, block.input)
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": content}
)
except Exception as err:
# Tell the model the call failed, so it does not invent a chart.
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(err),
"is_error": True,
}
)
# All results go back in ONE user message.
messages.append({"role": "user", "content": tool_results})
return "".join(block.text for block in response.content if block.type == "text")
if __name__ == "__main__":
print(
ask(
"I was born on 10 May 1990 at 19:55. Latitude 19.2056, longitude "
"25.2056, timezone +5.5. What is my rising sign, and where is my Moon?"
)
)Node
Install the SDK with npm install @anthropic-ai/sdk. Set the same environment variables, save the file as ai-astrologer.mjs and run node ai-astrologer.mjs.
// ai-astrologer.mjs - Node 18+ (built-in fetch)
// npm install @anthropic-ai/sdk
// Run this on your server. Never put your API keys in browser code.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // reads ANTHROPIC_API_KEY
const ASTRO_BASE = "https://json.astrologyapi.com/v1";
const SYSTEM_PROMPT =
"You are an astrology assistant. Always call get_birth_chart for planet, " +
"sign, house and aspect positions. Never guess a position. Use only values " +
"from the tool result. If the user has not given an exact birth date, time " +
"and place, ask for them before you call the tool.";
const tools = [
{
name: "get_birth_chart",
description:
"Returns a computed Western (tropical) birth chart from AstrologyAPI: " +
"each planet's sign, degree, house and retrograde flag, the 12 house " +
"cusps (Placidus), the ascendant, the midheaven and the aspects between " +
"planets. Call this before you state any planet, sign, house or aspect. " +
"Never estimate these values yourself.",
input_schema: {
type: "object",
properties: {
day: { type: "integer", description: "Day of birth, 1 to 31" },
month: { type: "integer", description: "Month of birth, 1 to 12" },
year: { type: "integer", description: "Year of birth, e.g. 1990" },
hour: { type: "integer", description: "Hour of birth, 24-hour local time, 0 to 23" },
min: { type: "integer", description: "Minute of birth, 0 to 59" },
lat: { type: "number", description: "Latitude of the birth place in decimal degrees, e.g. 19.2056" },
lon: { type: "number", description: "Longitude of the birth place in decimal degrees, e.g. 25.2056" },
tzone: { type: "number", description: "UTC offset in hours at the birth date and time, e.g. 5.5 or -5" },
},
required: ["day", "month", "year", "hour", "min", "lat", "lon", "tzone"],
},
},
];
// Call our western_horoscope endpoint and return the JSON as a string.
async function getBirthChart(birth) {
const headers = { "Content-Type": "application/json" }; // JSON body either way
if (process.env.ASTROLOGY_ACCESS_TOKEN) {
// Free-trial and wallet accounts: access token in a header.
headers["x-astrologyapi-key"] = process.env.ASTROLOGY_ACCESS_TOKEN;
} else {
// Plan subscribers: HTTP Basic auth with user ID and API key.
headers.Authorization =
"Basic " +
Buffer.from(
process.env.ASTROLOGY_USER_ID + ":" + process.env.ASTROLOGY_API_KEY,
).toString("base64");
}
const res = await fetch(ASTRO_BASE + "/western_horoscope", {
method: "POST",
headers,
body: JSON.stringify({
day: birth.day,
month: birth.month,
year: birth.year,
hour: birth.hour,
min: birth.min,
lat: birth.lat,
lon: birth.lon,
tzone: birth.tzone,
house_type: "placidus",
is_asteroids: false,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error("western_horoscope failed with " + res.status + ": " + text.slice(0, 200));
}
return JSON.stringify(await res.json());
}
async function runTool(name, input) {
if (name === "get_birth_chart") return getBirthChart(input); // input is already an object
throw new Error("Unknown tool: " + name);
}
async function ask(question) {
const messages = [{ role: "user", content: question }];
let response;
while (true) {
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
system: SYSTEM_PROMPT,
tools,
messages,
});
if (response.stop_reason === "refusal") {
return "Sorry, I can't help with that request.";
}
if (response.stop_reason === "end_turn") break;
if (response.stop_reason !== "tool_use") {
throw new Error("Model stopped early: " + response.stop_reason);
}
messages.push({ role: "assistant", content: response.content });
const toolResults = [];
for (const block of response.content.filter((b) => b.type === "tool_use")) {
try {
const content = await runTool(block.name, block.input);
toolResults.push({ type: "tool_result", tool_use_id: block.id, content });
} catch (err) {
// Tell the model the call failed, so it does not invent a chart.
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: String(err.message),
is_error: true,
});
}
}
// All results go back in ONE user message.
messages.push({ role: "user", content: toolResults });
}
return response.content
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("");
}
console.log(
await ask(
"I was born on 10 May 1990 at 19:55. Latitude 19.2056, longitude " +
"25.2056, timezone +5.5. What is my rising sign, and where is my Moon?",
),
);If our API returns an error, both examples send it back as a tool_result with is_error set. The model can then tell the user the chart is not available, instead of making one up. For retries and error codes, see the production checklist.
Using ChatGPT or other models
Most LLM providers support function calling, which is the same idea as tool calling. The JSON Schema in Step 1 carries over almost as is. Each provider wraps it in its own request format and names the fields a little differently. Check your provider's docs for the exact shape.
Your server function does not change. It still takes the eight birth fields, calls our API with your access token or Basic auth, and returns the JSON as a string.
If you use ChatGPT or another MCP client directly, you may not need any loop code. Connect the client to our MCP server and its tools appear in the chat.
Prompting tips
- Always call the tool. Say in the system prompt that every position must come from
get_birth_chart. The model must never guess a sign, degree, house or aspect. - Ask for the exact birth time. The rising sign changes about every two hours, and house positions depend on the minute. If the user does not know the time, read When the birth time is unknown.
- Don't let the model guess coordinates. A city name is not enough. Resolve the place in your own code with our geo_details and timezone_with_dst endpoints, then pass the numbers in.
- Keep the system prompt stable. Put the same text and the same tool list first in every request. Many providers can then reuse that prefix from a cache. Don't put timestamps or user names in the system prompt.
- Use only the tool result. Tell the model to say so when a value is missing, rather than fill the gap.
- Cache charts on your side. A chart for the same birth data never changes. Store it, and reuse it for follow-up questions.
What it costs
An AI astrologer has two costs. The first is our API. Each call to western_horoscope uses credits from your plan or wallet. Use the credits calculator to see the cost per endpoint, and pricing for plans. To pay only for what you use, see Pay as you go with a wallet.
The second cost is the LLM provider. It charges for the tokens in each request and reply, at its own prices. Tool results count as input tokens, so a full chart in every turn adds up. Caching charts and keeping the prompt stable both help.
Build your AI astrologer today
New accounts get 150 free credits, so you can test the tool loop before you pay anything.
Frequently asked questions
Can ChatGPT or Claude calculate a birth chart on its own?
No. A language model predicts text. It does not run the astronomy math, so the planet degrees and rising sign it writes are guesses. Give the model a tool that calls our API, and it can read the real positions instead.
Do I need the MCP server if I use tool calling?
No. Tool calling and the MCP server are two separate routes. With tool calling, your own code calls our JSON API. With MCP, an AI client such as Claude Desktop, Cursor or ChatGPT connects to our hosted server and calls the tools for you.
Which endpoint should my tool call?
For a Western chart, call western_horoscope. It returns planets, houses, the ascendant, the midheaven and aspects in one response. For a Vedic chart, call planets. Both take the same eight birth fields: day, month, year, hour, min, lat, lon and tzone.
Can I call the API straight from the browser?
No. We do not send CORS headers, and your access token or API key must stay secret. Run the tool function on your server. The browser talks to your server, and your server talks to us and to the LLM provider.
How much does an AI astrologer cost to run?
You pay two bills. Each call to our API uses credits from your plan or wallet, and the credits calculator shows the cost per endpoint. The LLM provider charges for its own tokens at its own prices. New accounts get 150 free credits to test with.
What if the user does not know their birth time?
Ask for it first. The rising sign and the houses depend on the exact time, so a wrong time gives a wrong chart. If the user truly does not know it, see our guide on unknown birth times before you show houses or an ascendant.
For AI agents
- Define one tool,
get_birth_chart, with required fieldsday,month,year,hour,min(integers) andlat,lon,tzone(numbers). - Western: POST
/western_horoscopewith the eight fields plushouse_typeandis_asteroids. Vedic: POST/planetswith the eight fields. - Base URL
https://json.astrologyapi.com/v1, JSON body. Auth: access token in thex-astrologyapi-keyheader (free trial and wallet), or HTTP Basic with user ID and API key (plan subscribers). Server-side only. No CORS. - Loop: on
tool_use, append the assistant content, run each tool, return alltool_resultblocks in one user message. Stop onend_turnorrefusal. - No loop code: use the MCP server at
https://mcp.astrologyapi.com/mcp. Fully hosted chat: use the Chat API. - Costs: our credits per call (see
/credits-calculator) plus the LLM provider's token prices.