🕓 Recorded 2026-04-14, updated 2026-05-09. TS / Python / PHP SDKs are now live on public registries —
@astroway/sdk(npm),astroway(PyPI),astroway/sdk(Packagist). Details in changelog.
You want to build an astrology app. Where do you start? This guide walks through the decisions and code — from picking an API to deploying a working product.
The architecture question
Section titled “The architecture question”Before writing any code, decide: are calculations client-side or server-side?
Client-side (e.g. Swiss Ephemeris WASM in the browser):
- Pros: no per-request cost, works offline, full control
- Cons: ~2MB WASM download per user, licensing complexity (commercial Swiss Ephemeris use has restrictions), you maintain the calculation code
Server-side API (AstroWay, Prokerala, similar):
- Pros: no client-side dependencies, small app bundle, fast on mobile, no licensing headaches
- Cons: per-request credit cost, latency on cold requests
For most apps, server-side via API wins. Mobile users won’t wait for a 2MB WASM blob; licensing issues aren’t worth navigating; and modern APIs cache identical requests for free (AstroWay does 5-minute caching with X-Cache: HIT).
Picking an API
Section titled “Picking an API”Factors that matter:
- Accuracy — look for Swiss Ephemeris under the hood (±1 arcsecond). If the API doesn’t say Swiss Ephemeris, ask.
- Coverage — natal + synastry + transits is the minimum. Progressions and returns are next. Rare techniques (rectification, Human Design) differentiate.
- SDK quality — typed TypeScript and Python SDKs save hours.
- Pricing model — credit-based scales better than per-request for mixed workloads.
I’ll use AstroWay API for this tutorial because it has all four (705 endpoints, Swiss Ephemeris, typed SDKs, credit-based).
Step 1: Set up the backend
Section titled “Step 1: Set up the backend”Never call an astrology API from the browser — your API key would leak. Always go through your backend.
mkdir my-astrology-appcd my-astrology-appnpm init -ynpm install express @astroway/sdk zodnpm install -D typescript @types/express @types/node tsxCreate src/server.ts:
import express from 'express';import { Astroway } from '@astroway/sdk';import { z } from 'zod';
const app = express();app.use(express.json());
const client = new Astroway({ apiKey: process.env.ASTROWAY_API_KEY!,});
const BirthInput = z.object({ date: z.string(), time: z.string(), timezoneOffset: z.number(), latitude: z.number(), longitude: z.number(),});
app.post('/api/chart', async (req, res) => { const parsed = BirthInput.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error });
try { const chart = await client.chart({ ...parsed.data, houseSystem: 'P', }); res.json(chart); } catch (err) { res.status(500).json({ error: String(err) }); }});
app.listen(3000, () => console.log('http://localhost:3000'));Run it:
ASTROWAY_API_KEY=aw_test_... npx tsx src/server.tsTest with curl:
curl -X POST http://localhost:3000/api/chart \ -H "Content-Type: application/json" \ -d '{"date":"1990-07-14","time":"14:30:00","timezoneOffset":3,"latitude":50.4501,"longitude":30.5234}'You get back a full natal chart JSON.
Step 2: Add a frontend
Section titled “Step 2: Add a frontend”Any framework works. For this example — React with a simple form:
import { useState } from 'react';
type Chart = { planets: { name: string; longitude: number; sign: string; house: number }[]; aspects: { planet1: string; planet2: string; type: string; orb: number }[];};
export function NatalForm() { const [chart, setChart] = useState<Chart | null>(null);
async function handleSubmit(formData: FormData) { const body = Object.fromEntries(formData.entries()); const res = await fetch('/api/chart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: body.date, time: body.time + ':00', timezoneOffset: Number(body.tz), latitude: Number(body.lat), longitude: Number(body.lng), }), }); setChart(await res.json()); }
return ( <div> <form action={handleSubmit}> <input name="date" type="date" required /> <input name="time" type="time" required /> <input name="tz" type="number" placeholder="Timezone offset (e.g. 3)" required /> <input name="lat" type="number" step="0.0001" placeholder="Latitude" required /> <input name="lng" type="number" step="0.0001" placeholder="Longitude" required /> <button>Build chart</button> </form>
{chart && ( <div> <h2>Planets</h2> <ul> {chart.planets.map(p => ( <li key={p.name}>{p.name}: {p.sign} (house {p.house})</li> ))} </ul> <h2>Aspects</h2> <ul> {chart.aspects.map((a, i) => ( <li key={i}>{a.planet1} {a.type} {a.planet2} (orb {a.orb.toFixed(2)}°)</li> ))} </ul> </div> )} </div> );}Step 3: Add synastry
Section titled “Step 3: Add synastry”Compatibility is the next obvious feature. Add to src/server.ts:
const SynastryInput = z.object({ chart1: BirthInput, chart2: BirthInput,});
app.post('/api/synastry', async (req, res) => { const parsed = SynastryInput.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error });
const result = await client.synastry(parsed.data); res.json({ score: result.compatibility.score, label: result.compatibility.label, aspects: result.crossAspects, });});That’s it. One endpoint, 50 credits per call, returns a 0–100 compatibility score with cross-aspects.
Step 4: Add daily horoscopes
Section titled “Step 4: Add daily horoscopes”For content-driven engagement, daily horoscopes are essential. AstroWay generates them from real transit data:
app.post('/api/horoscope/daily', async (req, res) => { const parsed = BirthInput.safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error });
const horoscope = await client.horoscopeDaily(parsed.data); res.json({ text: horoscope.text, disclaimer: horoscope.disclaimer, // preserve this in UI! });});Important: AI-generated content includes a disclaimer that you’re required to show users (Terms of Service). Don’t strip it.
Step 5: Caching
Section titled “Step 5: Caching”Daily horoscopes are the same for all users born on the same day with similar birth charts. Cache aggressively:
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({ max: 1000, ttl: 86400 * 1000 });
app.post('/api/horoscope/daily', async (req, res) => { const key = JSON.stringify(req.body); const cached = cache.get(key); if (cached) return res.json(cached);
const horoscope = await client.horoscopeDaily(req.body); cache.set(key, horoscope); res.json(horoscope);});AstroWay also has built-in 5-minute server-side caching (X-Cache: HIT), but for longer-lived content you want your own cache.
Step 6: Deploy
Section titled “Step 6: Deploy”Use any platform. For quick deployment, Vercel or Railway work:
# vercel.json{ "functions": { "src/server.ts": { "runtime": "@vercel/node" } } }Set the ASTROWAY_API_KEY env var in your platform dashboard.
Budget planning
Section titled “Budget planning”If you’ve made it this far, estimate your monthly credit use before shipping:
credits/month = DAU × (avg_charts_per_user × 20 + avg_synastries × 50 + avg_horoscopes × 20) × 30For 100 DAU with 1 chart + 1 horoscope + 0.3 synastries per user per day:
100 × (20 + 20 + 0.3 × 50) × 30 = 165,000 credits/monthThat’s the Starter plan at $19/mo (200K credits).
What’s next
Section titled “What’s next”Advanced features to add as your app grows:
- Transits overlay —
/v1/transitsshows current planetary positions on the user’s natal chart - Progressions —
/v1/progressionsfor psychological development arcs - Solar return —
/v1/solar-returnfor annual “birthday forecast” - Human Design — the Human Design API adds a whole new feature domain
- Astrocartography —
/v1/acgmaps where the user should live
Each adds a feature that’s hard to build from scratch but is one API call with AstroWay.
Start building
Section titled “Start building”- Get your free API key — 10,000 credits/month
- Full API reference — all 705 endpoints
- Birth Chart API — detailed chart endpoint documentation
Same Swiss Ephemeris as Solar Fire — but in 4 lines of code.
Free key, no card. 5,000 calls/month before you pay anything.