SDKs and Postman
Official TypeScript and Python clients with retries, idempotency and PDF job helpers built in, and a ready-to-run Postman collection.
Every SDK method and every Postman request is generated from the OpenAPI document, so they cover every capability in the API reference with the same names. The parts that make calls safe (retries, idempotency keys, errors, PDF jobs, webhook signatures) are written by hand and tested.
| TypeScript / JavaScript | Python | |
|---|---|---|
| Package | @96astro/api | astro96-api (import astro96_api) |
| Runtime | Node.js 18+ (and other runtimes with fetch). Zero dependencies | Python 3.9+, sync and asyncio clients on httpx |
| Formats | ESM and CommonJS, with type declarations | Typed (py.typed), TypedDict inputs |
Publishing soon. Both SDKs are built and tested, but they are not on npm or PyPI yet, so npm install and pip install will not find them today. Until they are listed, contact support for a pre-release package file, or call the API over plain HTTP as in the Quickstart. This page will drop this note when they are published.
Install
# TypeScript / JavaScript (once published)
npm install @96astro/api
# Python (once published)
pip install astro96-apiBoth clients read the key from the ASTRO_API_KEY environment variable when you do not pass one.
Keys belong on your server. The TypeScript client refuses to start in a browser unless you pass dangerouslyAllowBrowser: true, because a key in a web page is visible to every visitor.
Your first call
TypeScript
import { Astro96Client } from "@96astro/api";
const astro = new Astro96Client({ apiKey: process.env.ASTRO_API_KEY });
const { data, meta, rateLimit } = await astro.vedicCharts.planetPositions({
datetime: "1990-10-18T14:30:00",
timezone: "Asia/Kolkata",
latitude: 28.6139,
longitude: 77.209,
});
console.log(data.ascendant.sign, data.planets.length, meta.credits_used, rateLimit?.remaining);Python
from astro96_api import Astro96Client
astro = Astro96Client() # reads ASTRO_API_KEY
res = astro.vedic_charts.planet_positions(
{"datetime": "1990-10-18T14:30:00", "timezone": "Asia/Kolkata", "latitude": 28.6139, "longitude": 77.209}
)
print(res.data["ascendant"]["sign"], res.meta["credits_used"], res.rate_limit)Python, async
import asyncio
from astro96_api import AsyncAstro96Client
async def main() -> None:
async with AsyncAstro96Client() as astro:
res = await astro.panchang.daily_panchang(
{"date": "2026-10-15", "timezone": "Asia/Kolkata", "latitude": 28.6139, "longitude": 77.209}
)
print(res.data)
asyncio.run(main())Methods follow the URL: POST /v1/{family}/{capability} is client.family.capability(), in camelCase for TypeScript (astro.doshasYogas.manglikDosha) and snake_case for Python (astro.doshas_yogas.manglik_dosha). Each call returns the response's data and meta, plus requestId, rateLimit (the RateLimit-* headers), replayed and the idempotencyKey that was sent.
Types
Request bodies are typed from the OpenAPI request schemas, so your editor knows every field, its limits and its default. Response types (data) are inferred from the documented example response, not a contract: another response may omit a field, set it to null or carry more items. Check optional-looking fields before you rely on them. In Python, response data is a TypedDict of its top-level fields with nested objects as plain dicts.
Errors
A failed call raises an ApiError with the fields of the error envelope: status, code (stable: program against it), field, retryable and requestId. A network failure or timeout raises ConnectionError (ApiConnectionError in Python).
import { ApiError } from "@96astro/api";
try {
await astro.vedicCharts.planetPositions({ datetime: "1990-10-18T14:30:00", latitude: 99, longitude: 77.2 });
} catch (err) {
if (err instanceof ApiError && err.code === "invalid_request") {
console.error(`Fix ${err.field}: ${err.message} (request ${err.requestId})`);
} else {
throw err;
}
}from astro96_api import ApiError
try:
astro.vedic_charts.planet_positions({"datetime": "1990-10-18T14:30:00", "latitude": 99, "longitude": 77.2})
except ApiError as e:
if e.code == "quota_exhausted":
... # top up or upgrade; retrying will not help
print(e.status, e.code, e.field, e.request_id)Retries and idempotency
The clients retry for you, with exponential backoff and jitter, up to maxRetries (max_retries, default 2):
429 rate_limitedand503are retried after the server'sRetry-After. If it asks for longer thanmaxRetryDelayMs(max_retry_delay, default 30 seconds), the error is raised instead so you can decide.429 quota_exhaustedis never retried.- Other errors marked
retryable(500,502,504, an idempotency key still in progress) and network failures are retried only when the call carries an idempotency key, so a retry can never be charged twice.
Every POST gets an Idempotency-Key automatically, reused across the client's own retries. To retry safely across processes (say, a job queue), pass your own key and reuse it for the same logical operation:
import { idempotencyKey } from "@96astro/api";
const key = order.idempotencyKey ?? idempotencyKey(); // store it with the order
await astro.reports.reportCareer(birth, { idempotencyKey: key });from astro96_api import idempotency_key
key = order.idempotency_key or idempotency_key() # store it with the order
astro.reports.report_career(birth, idempotency_key=key)Turn automatic keys off with autoIdempotency: false (auto_idempotency=False). See Idempotency for how the API replays a repeated key.
Options
| TypeScript | Python | Default | Meaning |
|---|---|---|---|
apiKey | api_key | ASTRO_API_KEY | Your sandbox or live key |
baseUrl | base_url | https://api.96astro.com | API address |
auth | auth | "bearer" | "bearer" or "x-api-key" header |
timeoutMs | timeout | 60 s | Per attempt |
maxRetries | max_retries | 2 | Retries after the first attempt |
fetch | http_client | global fetch / new httpx client | Bring your own transport, proxy or mock |
Each method also takes per-call options: idempotencyKey, timeoutMs, maxRetries, headers and an AbortSignal to cancel (Python: idempotency_key=, timeout=, max_retries=, headers=; cancel an asyncio task to stop an async call).
PDF reports
createPdfJob, waitForJob and downloadFile wrap the PDF job routes. waitForJob polls with a growing interval (2 s to 10 s, up to 5 minutes by default) and raises JobError if the job fails. downloadFile fetches the signed link without your key and refreshes an expired link once.
import { writeFile } from "node:fs/promises";
const birth = { datetime: "1990-10-18T14:30:00", timezone: "Asia/Kolkata", latitude: 28.6139, longitude: 77.209 };
const { job, pdf } = await astro.renderPdf({
capability: "report-career",
input: birth,
branding: { brand_name: "Shree Jyotish Kendra", accent: "#1f4fd1" },
});
await writeFile(`career-${job.id}.pdf`, pdf);job = astro.create_pdf_job("report-career", birth, branding={"brand_name": "Shree Jyotish Kendra"}).data
done = astro.wait_for_job(job, timeout=300)
with open(f"career-{done['id']}.pdf", "wb") as f:
f.write(astro.download_file(done))Webhooks
verifyWebhook (verify_webhook) checks the Astro-Signature of a webhook against your endpoint's secret (or several, while you rotate), refuses timestamps more than 5 minutes off, compares in constant time and returns the parsed event. Pass the raw request body.
import express from "express";
import { verifyWebhook, WebhookSignatureError } from "@96astro/api";
app.post("/webhooks/96astro", express.raw({ type: "application/json" }), async (req, res) => {
try {
const event = await verifyWebhook({ rawBody: req.body, headers: req.headers, secret: process.env.ASTRO_WEBHOOK_SECRET! });
// Deduplicate on event.id, queue the work, then answer quickly.
res.sendStatus(204);
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send("bad signature");
throw err;
}
});from astro96_api import WebhookSignatureError, verify_webhook
try:
event = verify_webhook(request.body, request.headers, os.environ["ASTRO_WEBHOOK_SECRET"])
except WebhookSignatureError:
return 400Postman
Download the Postman collection and an environment (Sandbox or Live), then import them in Postman (Import, drop the files).
- Select the environment and paste your key into
api_key. The files carry no key. Start with a sandbox key (sk_test_...): calls are not charged. - The collection authenticates every request with
Authorization: Bearer {{api_key}}and sends a freshIdempotency-Key({{$guid}}). Replace it with a fixed value to try a replay. - Requests are grouped by family, with the documented example body and a saved example response.
- The PDF jobs folder chains create, poll and download: its scripts carry
job_idanddownload_urlfrom one request to the next.
Other languages
Any language with an HTTP client can call the API: see the Quickstart and Requests and responses. The OpenAPI 3.1 document also works with generators such as OpenAPI Generator.