Documentation
Streaming and AI chat
Stream AI readings as Server-Sent Events, hold multi-turn conversations about one chart, and read the citations that tie each fact to the data behind it.
AI readings (ai-insight, ai-chat, ai-lens, ai-general) can be streamed word by word, and a conversation keeps the turns of a chat about one chart so you do not have to send the history yourself. Both are billed exactly like an ordinary JSON reading: by the model's measured cost, inside the tier's minimum and ceiling (see Credits and rate limits).
Streaming a reading
Ask for a stream in either of two ways:
"stream": truein the JSON body (the convention used by OpenAI's and Anthropic's APIs), or- the header
Accept: text/event-stream(the Server-Sent Events standard).
The body flag wins when both are given, so "stream": false always returns JSON. json_mode cannot be streamed (422). A capability that is not an AI text reading answers 422 to "stream": true and 406 not_acceptable to the header.
The response is text/event-stream. Every event has a name and one JSON object as its data:
event: start
data: {"request_id":"req_...","capability":"ai-insight","ai":{"provider":"anthropic","model":"...","tier":"fast"}}
event: delta
data: {"text":"Your chart shows "}
event: delta
data: {"text":"a strong 10th house [S12]."}
event: done
data: {"data":{"text":"Your chart shows a strong 10th house [S12].","citations":[...],...},"meta":{"credits_used":93,...}}| Event | When | Data |
|---|---|---|
start | Once, before any text | request_id, capability, ai (provider, model, tier) |
delta | Many times | text: the next piece of the answer. Join them in order. |
done | Once, at the end of a successful reading | The full envelope, exactly what the JSON call returns: data (text, citations ...) and meta (credits_used, ai usage, warnings) |
error | Instead of done when the reading fails part-way | error (code, message, retryable, request_id) and meta.credits_used: 0 |
A stream that fails before its first word (no AI provider available, out of credits, bad input) is an ordinary JSON error response with its HTTP status, never a stream.
What a stream costs
- Finished: the same credits as the same reading as JSON, settled on the model's real token usage when the answer ends.
done.meta.credits_usedis the charge. - Provider error part-way (
errorevent): nothing is charged; the whole reservation is released. Retry it. - You close the connection part-way: the model is stopped at once. If no text had reached you, nothing is charged. If some had, you pay for what was generated so far: the prompt tokens plus about one output token per four characters delivered, at the model's rates, within the tier's minimum and ceiling. The call is recorded in your usage as failed (
499 client_disconnected) with that charge. - Retry with the same
Idempotency-Key: the stored answer is streamed again (start, the whole text as onedelta,donewithmeta.replayed: true), and is not charged again. A stream you left part-way is not re-run under its key; use a new key.
The credit headers (X-Credits-Used, X-Credits-Remaining) are sent before the answer exists, so a stream does not carry them; read done.meta.credits_used instead.
Reading a stream
curl
curl -N https://api.96astroapi.com/v1/ai/ai-insight \
-H "Authorization: Bearer $ASTRO_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"datetime":"1990-10-18T14:30:00","timezone":"Asia/Kolkata","latitude":28.6139,"longitude":77.2090,"tier":"fast"}'-N turns off curl's buffering so each event prints as it arrives.
Node
// Node 18+ (global fetch). EventSource cannot send a POST body or a header, so read the stream yourself.
const res = await fetch("https://api.96astroapi.com/v1/ai/ai-insight", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ASTRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
datetime: "1990-10-18T14:30:00", timezone: "Asia/Kolkata",
latitude: 28.6139, longitude: 77.209, tier: "fast", stream: true,
}),
});
if (!res.headers.get("content-type")?.startsWith("text/event-stream")) {
throw new Error(`Not a stream: ${res.status} ${await res.text()}`);
}
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let end;
while ((end = buffer.indexOf("\n\n")) !== -1) {
const block = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const event = /^event: (.*)$/m.exec(block)?.[1];
const data = JSON.parse(/^data: (.*)$/m.exec(block)?.[1] ?? "{}");
if (event === "delta") process.stdout.write(data.text);
if (event === "done") console.log("\ncredits:", data.meta.credits_used, data.data.citations);
if (event === "error") console.error("\nfailed:", data.error.code);
}
}Python
import json, os
import httpx
body = {"datetime": "1990-10-18T14:30:00", "timezone": "Asia/Kolkata",
"latitude": 28.6139, "longitude": 77.2090, "tier": "fast", "stream": True}
headers = {"Authorization": f"Bearer {os.environ['ASTRO_API_KEY']}"}
with httpx.stream("POST", "https://api.96astroapi.com/v1/ai/ai-insight",
json=body, headers=headers, timeout=180) as r:
if not r.headers["content-type"].startswith("text/event-stream"):
r.read()
raise RuntimeError(f"{r.status_code}: {r.text}")
event = None
for line in r.iter_lines():
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
data = json.loads(line[6:])
if event == "delta":
print(data["text"], end="", flush=True)
elif event == "done":
print("\ncredits:", data["meta"]["credits_used"])
elif event == "error":
print("\nfailed:", data["error"]["code"])Behind a proxy, turn off response buffering for these calls (for nginx, the API already sends X-Accel-Buffering: no).
Questions are answered from the matching calculations
For ai-chat and ai-general, the question decides what the answer is computed from. A question about marriage timing ("When will I get married?", "shaadi kab hogi", "शादी कब होगी?") computes marriage-timing and the running dasha before the model writes anything. A career question computes career-timing or career-fields, and a question about a year computes that year's varshaphal. "When will we have a child?" computes children-timing and "when will I buy a house?" computes property-timing, each with the running dasha. Health, foreign travel, property, children, education, love, Sade Sati and doshas each have their own calculations. The model explains those results; it does not work the dates out itself.
A question about something that is not going well is answered differently. "My marriage is not happening", "meri shaadi kyu nahi ho rahi", "no growth in my job", "we are trying for a baby" or "court case kab khatam hoga" computes what explains it, when it eases and what is traditionally done:
| Difficulty | Computed |
|---|---|
| Marriage delayed or not happening | marriage-timing, manglik-dosha, puja-suggestion |
| Trouble inside a marriage (quarrels, separation, in-laws) | report-marriage, manglik-dosha, puja-suggestion. With the spouse's birth details too, both charts can be read |
| Career stuck, job loss, no promotion | career-timing, sade-sati-current, puja-suggestion |
| Debt or losses | report-wealth, dasha-current, sade-sati-current |
| Children delayed | children-timing, parashari-yoga-children-yogas, puja-suggestion |
| Visa or settling abroad | foreign-timing, dasha-current, puja-suggestion |
| Property or a house not coming through | property-timing, dasha-current, puja-suggestion |
| Love, studies | that topic's report, dasha-current, puja-suggestion |
| A legal case | dasha-current, sade-sati-current, puja-suggestion |
| "Nothing is going right" | dasha-current, sade-sati-current, puja-suggestion |
The answer explains the chart's reasons (the running dasha, Saturn's transit, a dosha, the house and its lord), then the window in which things ease, then remedies and practical steps. It does not frighten, does not promise that a remedy guarantees a result, and does not push paid rituals.
If someone writes that they want to end their life, are in despair, or are being harmed, the answer gives no reading at all. It offers support first, with a crisis line (in India, Tele-MANAS 14416), never treats the stars as a cause or excuse for abuse, and never suggests enduring harm as a remedy.
Calendar questions are computed too:
| Question | Computed |
|---|---|
| Festivals ("Diwali kab hai?", "when is Holi 2027", Pongal, Makar Sankranti, Lohri, Onam). Without a year, the next one | festival-calendar |
| Other religions' dates: Eid, Ramadan, Muharram, Easter, Christmas, Lent, Gurpurab, Vaisakhi, Paryushan, Samvatsari, Vesak | religious-calendar for that tradition |
| Vrats ("next Ekadashi", "Purnima dates") | ekadashi-calendar or vrat-calendar |
| Moonrise ("aaj chand kab niklega"; for Karwa Chauth, on the festival's own date) | moonrise |
| Today's or a given day's panchang, Rahu Kaal and choghadiya | daily-panchang |
| A muhurta for a purpose (a wedding, griha pravesh, a vehicle, a business, mundan…), in the coming weeks or a named month | muhurta-finder |
| A wedding plan with mehendi, haldi, sangeet and the rest, for Hindu communities by region and for Sikh, Jain, Muslim, Christian, Parsi and Buddhist weddings | wedding-plan |
Send partner (the partner's datetime, latitude, longitude, and timezone or utc_offset) to have compatibility and marriage questions read from both charts, with partner_role (bride by default) saying which side the partner takes in Vedic matching. "Are we compatible?" then computes matching-report and manglik-match. "We fight every day, will our marriage survive?" computes marriage-sustainability, matching-report and puja-suggestion. The answer speaks about both people fairly and never blames one of them. Without partner, the answer says it needs the partner's birth details.
Send place (latitude, longitude, and timezone or utc_offset) for where the person is now. Without it, calendar answers are for the birth place, and the answer says so. Pure calendar questions (festival, vrat, moonrise, panchang) leave the birth chart out, because they are about the sky at a place, not the person.
data.routing shows what happened:
| Field | Meaning |
|---|---|
intents | The topics the question asks about, for example ["marriage"] |
calculations | The capabilities computed to answer it. Their facts carry citations like any other |
asks_timing | Whether the question asks when |
describes_problem | Whether the question describes a difficulty, which brings in the remedy and easing-window rules |
missing | What the answer still needs. A compatibility question needs the partner's birth details, and a muhurta needs a date range |
Rules that every routed answer follows:
- Timing is always a window with its reasons, never one guaranteed date. Asked for an exact date, the answer says no method can promise one and gives the strongest computed window.
- Health answers describe tendencies, not diagnoses, and say so.
- The AI does not predict death or lifespan.
A question with no specific topic ("Explain my chart") is answered from the whole chart, as before. Set context to choose the grounding yourself; it overrides the routing.
Citations
Every AI text reading returns data.citations. The model is given the chart data as numbered lines and asked to put the line's tag after each fact it states, like [S12]; the API keeps only tags that exist in that data (an invented one is removed from the text, with a warning) and turns each into the capability and field it came from:
"citations": [
{"ref": "S12", "capability": "planet-positions", "path": "data.planets[4]",
"endpoint": "/v1/vedic-charts/planet-positions"}
]path is a field path into that capability's own response, so you can call endpoint with the same birth details and show the exact value next to the sentence. The tags stay in data.text as footnote anchors; strip them with /\[S\d+\]/g if you do not show footnotes. General astrological meaning (what a house signifies) carries no tag. json_mode readings have no citations.
Conversations
A conversation pins one chart and keeps the turns, so each question is answered with the earlier ones as context.
| Call | Does | Cost |
|---|---|---|
POST /v1/conversations | Create one: the birth details (as for ai-chat) plus optional title, tier, persona, language, context as defaults | Free |
GET /v1/conversations?limit=20 | List yours, most recently active first (items, has_more) | Free |
GET /v1/conversations/{id} | One conversation with all its messages | Free |
POST /v1/conversations/{id}/messages | Ask: {"question": "...", "tier"?, "persona"?, "language"?, "stream"?} | An ai-chat reading |
DELETE /v1/conversations/{id} | Delete it and its messages now | Free |
# Create
curl https://api.96astroapi.com/v1/conversations \
-H "Authorization: Bearer $ASTRO_API_KEY" -H "Content-Type: application/json" \
-d '{"datetime":"1990-10-18T14:30:00","timezone":"Asia/Kolkata","latitude":28.6139,"longitude":77.2090,"tier":"fast","title":"Career"}'
# Ask (add "stream": true, or Accept: text/event-stream, to stream it)
curl https://api.96astroapi.com/v1/conversations/$ID/messages \
-H "Authorization: Bearer $ASTRO_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"question":"When is a good time to change jobs?"}'A message's answer is the ai-chat envelope plus data.conversation: id, message_id, message_count, and history (turns_sent, turns_omitted). Each stored assistant message keeps its citations, credits_used and request_id.
Rules
- Billing: a message is an
ai-chatreading and appears in your usage asai-chat, charged exactly as/v1/ai/ai-chat. A failed reading stores nothing and costs nothing. - History: the newest turns that fit in 6 turns and 4,000 characters (about 1,000 tokens) are sent with each question. Older turns are left out whole, never summarised (a summary would be a hidden, billed model call); a single over-long earlier answer is cut to fit. Citation tags are removed from earlier answers before they are sent.
- Scope: a conversation belongs to the organization, environment (test or live) and project of the key that created it. Any key of that project and environment can use it; every other key gets
404. Secret keys only: publishable keys get403 publishable_key_not_allowed, and a key scoped withoutai-chatgets403 key_scope_denied. - Retries: send an
Idempotency-Keywith each message. A retry with the same key and question replays the stored answer (meta.replayed: true) and adds no turn; the same key with another question is409 idempotency_conflict. - Size: at most 200 messages per conversation (
409 conversation_full); start a new one. - Retention: a conversation and its messages are deleted 30 days after the last message (
expires_aton each conversation says when), at once onDELETE, and with your account. Conversations hold birth details and personal questions: delete them when your user asks.


