96Astro API

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": true in 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,...}}
EventWhenData
startOnce, before any textrequest_id, capability, ai (provider, model, tier)
deltaMany timestext: the next piece of the answer. Join them in order.
doneOnce, at the end of a successful readingThe full envelope, exactly what the JSON call returns: data (text, citations ...) and meta (credits_used, ai usage, warnings)
errorInstead of done when the reading fails part-wayerror (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_used is the charge.
  • Provider error part-way (error event): 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 one delta, done with meta.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:

DifficultyComputed
Marriage delayed or not happeningmarriage-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 promotioncareer-timing, sade-sati-current, puja-suggestion
Debt or lossesreport-wealth, dasha-current, sade-sati-current
Children delayedchildren-timing, parashari-yoga-children-yogas, puja-suggestion
Visa or settling abroadforeign-timing, dasha-current, puja-suggestion
Property or a house not coming throughproperty-timing, dasha-current, puja-suggestion
Love, studiesthat topic's report, dasha-current, puja-suggestion
A legal casedasha-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:

QuestionComputed
Festivals ("Diwali kab hai?", "when is Holi 2027", Pongal, Makar Sankranti, Lohri, Onam). Without a year, the next onefestival-calendar
Other religions' dates: Eid, Ramadan, Muharram, Easter, Christmas, Lent, Gurpurab, Vaisakhi, Paryushan, Samvatsari, Vesakreligious-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 choghadiyadaily-panchang
A muhurta for a purpose (a wedding, griha pravesh, a vehicle, a business, mundan…), in the coming weeks or a named monthmuhurta-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 weddingswedding-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:

FieldMeaning
intentsThe topics the question asks about, for example ["marriage"]
calculationsThe capabilities computed to answer it. Their facts carry citations like any other
asks_timingWhether the question asks when
describes_problemWhether the question describes a difficulty, which brings in the remedy and easing-window rules
missingWhat 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.

CallDoesCost
POST /v1/conversationsCreate one: the birth details (as for ai-chat) plus optional title, tier, persona, language, context as defaultsFree
GET /v1/conversations?limit=20List yours, most recently active first (items, has_more)Free
GET /v1/conversations/{id}One conversation with all its messagesFree
POST /v1/conversations/{id}/messagesAsk: {"question": "...", "tier"?, "persona"?, "language"?, "stream"?}An ai-chat reading
DELETE /v1/conversations/{id}Delete it and its messages nowFree
# 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-chat reading and appears in your usage as ai-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 get 403 publishable_key_not_allowed, and a key scoped without ai-chat gets 403 key_scope_denied.
  • Retries: send an Idempotency-Key with 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 is 409 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_at on each conversation says when), at once on DELETE, and with your account. Conversations hold birth details and personal questions: delete them when your user asks.

On this page