Try Astrologer API

Subscribe to support and grow the project.

How to Build an Astrology App: Full Developer Tutorial #

Building an astrology app from scratch involves four distinct technical challenges: astronomical calculations, chart visualization, relationship analysis, and interpretive content generation. Trying to implement all of these yourself means months of work with the Swiss Ephemeris, SVG geometry, and astrological domain knowledge. Using the Astrologer API as your backend, you can focus entirely on your app’s user experience while the API handles every calculation and rendering task.

This tutorial walks through the complete architecture of an astrology app, from project setup to deployment, with working code in Python and JavaScript at each stage.

Architecture Overview #

A typical astrology app has four layers:

+------------------+
|   Frontend       |  User interface, forms, chart display
+------------------+
        |
+------------------+
|   Your Backend   |  Auth, user storage, API key management
+------------------+
        |
+------------------+
|  Astrologer API  |  Calculations, charts, AI context
+------------------+
        |
+------------------+
|  Swiss Ephemeris |  (handled by the API)
+------------------+

Your backend sits between your frontend and the Astrologer API. This layer is important because:

  1. It keeps your RapidAPI key server-side (never expose it in client code).
  2. It stores user birth data so users do not re-enter it every time.
  3. It caches API responses for repeat requests.
  4. It can combine multiple API calls into a single endpoint for your frontend.

Step 1: Get Your API Key #

Before writing any code, sign up for the Astrologer API on RapidAPI.

Get your API key on RapidAPI

Once subscribed, you will receive an API key that goes in the X-RapidAPI-Key header of every request.

Step 2: Set Up the API Client #

Create a reusable API client that handles authentication, error handling, and request formatting.

Python (Backend) #

import requests
from typing import Any

class AstrologerAPI:
    """Client for the Astrologer API v5."""

    BASE_URL = "https://astrologer.p.rapidapi.com/api/v5"

    def __init__(self, api_key: str):
        self.headers = {
            "Content-Type": "application/json",
            "X-RapidAPI-Key": api_key,
            "X-RapidAPI-Host": "astrologer.p.rapidapi.com"
        }

    def _post(self, endpoint: str, payload: dict) -> dict:
        """Make a POST request and return the parsed response."""
        url = f"{self.BASE_URL}/{endpoint}"
        response = requests.post(url, json=payload, headers=self.headers)
        response.raise_for_status()
        data = response.json()
        if data.get("status") != "OK":
            raise ValueError(f"API error: {data}")
        return data

    def get_subject(self, subject: dict) -> dict:
        """Calculate planetary positions for a birth moment."""
        return self._post("subject", {"subject": subject})

    def get_natal_chart(self, subject: dict, theme: str = "classic",
                        **options) -> dict:
        """Generate a natal chart SVG with computed data."""
        return self._post("chart/birth-chart", {
            "subject": subject, "theme": theme, **options
        })

    def get_natal_data(self, subject: dict,
                       distribution_method: str = "weighted") -> dict:
        """Get full natal chart data (aspects, distributions)."""
        return self._post("chart-data/birth-chart", {
            "subject": subject,
            "distribution_method": distribution_method
        })

    def get_synastry_chart(self, first: dict, second: dict,
                           theme: str = "classic", **options) -> dict:
        """Generate a synastry bi-wheel chart."""
        return self._post("chart/synastry", {
            "first_subject": first,
            "second_subject": second,
            "theme": theme, **options
        })

    def get_compatibility_score(self, first: dict, second: dict) -> dict:
        """Get a numerical compatibility score between two subjects."""
        return self._post("compatibility-score", {
            "first_subject": first,
            "second_subject": second
        })

    def get_natal_context(self, subject: dict) -> dict:
        """Get AI-generated natal chart interpretation."""
        return self._post("context/birth-chart", {"subject": subject})

    def get_synastry_context(self, first: dict, second: dict) -> dict:
        """Get AI-generated synastry interpretation."""
        return self._post("context/synastry", {
            "first_subject": first, "second_subject": second
        })

    def get_transit_context(self, natal: dict, transit: dict) -> dict:
        """Get AI-generated transit interpretation."""
        return self._post("context/transit", {
            "first_subject": natal, "transit_subject": transit
        })

    def get_moon_phase(self, year: int, month: int, day: int,
                       hour: int, minute: int, lat: float, lng: float,
                       tz: str) -> dict:
        """Get detailed moon phase data."""
        return self._post("moon-phase", {
            "year": year, "month": month, "day": day,
            "hour": hour, "minute": minute,
            "latitude": lat, "longitude": lng, "timezone": tz
        })


# Initialize with your API key
api = AstrologerAPI("YOUR_API_KEY")

JavaScript (Node.js Backend) #

class AstrologerAPI {
  static BASE_URL = "https://astrologer.p.rapidapi.com/api/v5";

  constructor(apiKey) {
    this.headers = {
      "Content-Type": "application/json",
      "X-RapidAPI-Key": apiKey,
      "X-RapidAPI-Host": "astrologer.p.rapidapi.com"
    };
  }

  async _post(endpoint, payload) {
    const response = await fetch(`${AstrologerAPI.BASE_URL}/${endpoint}`, {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify(payload)
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status} ${response.statusText}`);
    }

    const data = await response.json();
    if (data.status !== "OK") {
      throw new Error(`API error: ${JSON.stringify(data)}`);
    }
    return data;
  }

  async getSubject(subject) {
    return this._post("subject", { subject });
  }

  async getNatalChart(subject, theme = "classic", options = {}) {
    return this._post("chart/birth-chart", { subject, theme, ...options });
  }

  async getNatalData(subject, distributionMethod = "weighted") {
    return this._post("chart-data/birth-chart", {
      subject,
      distribution_method: distributionMethod
    });
  }

  async getSynastryChart(first, second, theme = "classic", options = {}) {
    return this._post("chart/synastry", {
      first_subject: first,
      second_subject: second,
      theme,
      ...options
    });
  }

  async getCompatibilityScore(first, second) {
    return this._post("compatibility-score", {
      first_subject: first,
      second_subject: second
    });
  }

  async getNatalContext(subject) {
    return this._post("context/birth-chart", { subject });
  }

  async getSynastryContext(first, second) {
    return this._post("context/synastry", {
      first_subject: first,
      second_subject: second
    });
  }

  async getTransitContext(natal, transit) {
    return this._post("context/transit", {
      first_subject: natal,
      transit_subject: transit
    });
  }

  async getMoonPhase(year, month, day, hour, minute, lat, lng, tz) {
    return this._post("moon-phase", {
      year, month, day, hour, minute,
      latitude: lat, longitude: lng, timezone: tz
    });
  }
}

const api = new AstrologerAPI("YOUR_API_KEY");

Step 3: Build the Natal Chart Feature #

The natal chart is the core feature of any astrology app. It requires a birth data form and a chart display component.

Defining the Subject Object #

The subject object is the standard input across all endpoints:

user_birth_data = {
    "name": "Alice",
    "year": 1990,
    "month": 3,
    "day": 21,
    "hour": 14,
    "minute": 30,
    "city": "New York",
    "nation": "US",
    "longitude": -74.006,
    "latitude": 40.7128,
    "timezone": "America/New_York"
}

See the subject endpoint docs for all available fields including zodiac_type, houses_system_identifier, and sidereal_mode.

Fetching and Displaying the Chart #

# Get the natal chart with SVG and data
chart = api.get_natal_chart(user_birth_data, theme="classic")

# Save the SVG for display
svg_markup = chart["chart"]

# Extract key data for the UI
subject = chart["chart_data"]["subject"]
aspects = chart["chart_data"]["aspects"]

birth_profile = {
    "sun": f"{subject['sun']['sign']} in {subject['sun']['house']}",
    "moon": f"{subject['moon']['sign']} in {subject['moon']['house']}",
    "rising": subject["ascendant"]["sign"],
    "house_system": subject["houses_system_name"],
    "total_aspects": len(aspects)
}

print(birth_profile)

Output:

{
    'sun': 'Ari in Tenth_House',
    'moon': 'Sco in Fifth_House',
    'rising': 'Can',
    'house_system': 'Placidus',
    'total_aspects': 42
}

JavaScript: Rendering the SVG in a Web App #

async function displayNatalChart(containerId, birthData) {
  const result = await api.getNatalChart(birthData);

  // Render the SVG
  const container = document.getElementById(containerId);
  container.innerHTML = result.chart;

  // Make SVG responsive
  const svg = container.querySelector("svg");
  if (svg) {
    svg.setAttribute("width", "100%");
    svg.setAttribute("height", "auto");
    svg.style.maxWidth = "600px";
  }

  // Build a summary for the sidebar
  const s = result.chart_data.subject;
  return {
    sun: `${s.sun.sign} (${s.sun.house})`,
    moon: `${s.moon.sign} (${s.moon.house})`,
    rising: s.ascendant.sign,
    aspects: result.chart_data.aspects
  };
}

Step 4: Add Synastry (Relationship Compatibility) #

Synastry is the second most popular feature in astrology apps. It compares two birth charts to analyze relationship dynamics.

Getting the Compatibility Score #

The compatibility score endpoint returns a numerical score with a rule-by-rule breakdown.

partner_a = {
    "name": "Alice",
    "year": 1990, "month": 3, "day": 21,
    "hour": 14, "minute": 30,
    "city": "New York", "nation": "US",
    "longitude": -74.006, "latitude": 40.7128,
    "timezone": "America/New_York"
}

partner_b = {
    "name": "Bob",
    "year": 1992, "month": 5, "day": 15,
    "hour": 18, "minute": 30,
    "city": "London", "nation": "GB",
    "longitude": -0.1278, "latitude": 51.5074,
    "timezone": "Europe/London"
}

score = api.get_compatibility_score(partner_a, partner_b)

print(f"Score: {score['score']} / 44")
print(f"Description: {score['score_description']}")
print(f"Destiny sign: {score['is_destiny_sign']}")
print(f"\nScore breakdown:")
for rule in score["score_breakdown"]:
    print(f"  +{rule['points']} -- {rule['description']}")

Example output:

Score: 16 / 44
Description: Very Important
Destiny sign: False

Score breakdown:
  +4 -- Sun-Moon sextile
  +4 -- Sun-Ascendant sextile
  +4 -- Moon-Ascendant trine
  +4 -- Venus-Mars sextile

Generating the Synastry Chart #

synastry = api.get_synastry_chart(partner_a, partner_b)

# Save the bi-wheel SVG
with open("synastry_chart.svg", "w") as f:
    f.write(synastry["chart"])

# List inter-chart aspects
aspects = synastry["chart_data"]["aspects"]
print(f"Inter-chart aspects: {len(aspects)}")
for a in aspects[:5]:
    print(f"  {a['p1_name']} ({a['p1_owner']}) {a['aspect']} "
          f"{a['p2_name']} ({a['p2_owner']}) -- orb: {a['orbit']:.2f}")

JavaScript: Synastry in One Call #

async function displaySynastry(containerId, partnerA, partnerB) {
  // Fetch chart and score in parallel
  const [chartResult, scoreResult] = await Promise.all([
    api.getSynastryChart(partnerA, partnerB),
    api.getCompatibilityScore(partnerA, partnerB)
  ]);

  // Render bi-wheel SVG
  const container = document.getElementById(containerId);
  container.innerHTML = chartResult.chart;

  return {
    score: scoreResult.score,
    maxScore: 44,
    description: scoreResult.score_description,
    isDestinySign: scoreResult.is_destiny_sign,
    breakdown: scoreResult.score_breakdown,
    aspects: chartResult.chart_data.aspects
  };
}

Step 5: Add AI-Powered Interpretations #

Raw data is powerful for developers, but users want readable interpretations. The context endpoints generate XML-structured interpretations optimized for feeding into LLMs.

Natal Chart Reading #

context = api.get_natal_context(user_birth_data)

# The XML context contains structured interpretation data
xml_context = context["context"]
print(f"Context size: {len(xml_context)} characters")

# You can use this XML as input to your LLM of choice:
#
# prompt = f"""Based on the following astrological chart analysis,
# write a personalized birth chart reading for the user.
# Keep the tone warm and insightful.
#
# {xml_context}"""
#
# response = your_llm.generate(prompt)

Synastry Interpretation #

synastry_context = api.get_synastry_context(partner_a, partner_b)
xml = synastry_context["context"]

# The XML includes:
# - Both subjects' full chart data
# - All inter-chart aspects with orbs
# - House overlay (each person's planets projected into the other's houses)
# - Compatibility score and element/quality distributions

See the synastry context docs for the full XML structure.

Transit Forecasts #

from datetime import datetime

now = datetime.utcnow()
transit_subject = {
    "name": "Transit",
    "year": now.year, "month": now.month, "day": now.day,
    "hour": now.hour, "minute": now.minute,
    "city": "New York", "nation": "US",
    "longitude": -74.006, "latitude": 40.7128,
    "timezone": "America/New_York"
}

transit = api.get_transit_context(user_birth_data, transit_subject)
transit_xml = transit["context"]

# Feed to LLM for a daily horoscope

See the transit context docs for details on the transit interpretation structure.

Step 6: Display SVG Charts in Your App #

The API returns SVG as a string. Here are patterns for common frameworks.

React #

function AstrologyChart({ svgMarkup }) {
  return (
    <div
      className="chart-container"
      style={{ maxWidth: "600px", margin: "0 auto" }}
      dangerouslySetInnerHTML={{ __html: svgMarkup }}
    />
  );
}

// Usage in a page component
function BirthChartPage({ birthData }) {
  const [chartData, setChartData] = useState(null);

  useEffect(() => {
    fetch("/api/natal-chart", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(birthData)
    })
      .then(r => r.json())
      .then(setChartData);
  }, [birthData]);

  if (!chartData) return <p>Loading chart...</p>;

  return (
    <div>
      <AstrologyChart svgMarkup={chartData.chart} />
      <p>Sun: {chartData.chart_data.subject.sun.sign}</p>
      <p>Moon: {chartData.chart_data.subject.moon.sign}</p>
      <p>Rising: {chartData.chart_data.subject.ascendant.sign}</p>
    </div>
  );
}

Plain HTML #

<div id="chart-display" style="max-width: 600px; margin: 0 auto;"></div>

<script>
  async function loadChart() {
    const res = await fetch("/api/natal-chart", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(birthData)
    });
    const data = await res.json();
    document.getElementById("chart-display").innerHTML = data.chart;
  }
  loadChart();
</script>

Step 7: Add Moon Phase Data #

The moon phase endpoint adds depth to your app with lunar cycle information.

moon = api.get_moon_phase(
    2026, 4, 21, 12, 0,
    40.7128, -74.006, "America/New_York"
)

overview = moon["moon_phase_overview"]
moon_data = overview["moon"]
sun_data = overview["sun"]

print(f"Moon phase: {moon_data['phase_name']} {moon_data['emoji']}")
print(f"Illumination: {moon_data['illumination']}")
print(f"Moon sign: {moon_data['zodiac']['moon_sign']}")
print(f"Sunrise: {sun_data['sunrise_timestamp']}")
print(f"Sunset: {sun_data['sunset_timestamp']}")

# Upcoming phases
phases = moon_data["detailed"]["upcoming_phases"]
next_full = phases["full_moon"]["next"]
print(f"Next full moon: {next_full['datestamp']}")

Step 8: Putting It All Together #

Here is a complete backend endpoint that combines multiple API calls into a single user profile response.

Python (Flask Example) #

from flask import Flask, request, jsonify

app = Flask(__name__)
api = AstrologerAPI("YOUR_API_KEY")

@app.route("/api/profile", methods=["POST"])
def user_profile():
    """Generate a complete astrological profile for a user."""
    birth = request.json

    subject = {
        "name": birth["name"],
        "year": birth["year"],
        "month": birth["month"],
        "day": birth["day"],
        "hour": birth["hour"],
        "minute": birth["minute"],
        "city": birth["city"],
        "nation": birth["nation"],
        "longitude": birth["longitude"],
        "latitude": birth["latitude"],
        "timezone": birth["timezone"]
    }

    # Fetch natal chart and AI context in parallel (use threading or async)
    chart = api.get_natal_chart(subject)
    context = api.get_natal_context(subject)

    s = chart["chart_data"]["subject"]

    return jsonify({
        "chart_svg": chart["chart"],
        "profile": {
            "sun": {"sign": s["sun"]["sign"], "house": s["sun"]["house"]},
            "moon": {"sign": s["moon"]["sign"], "house": s["moon"]["house"]},
            "rising": {"sign": s["ascendant"]["sign"]},
            "house_system": s["houses_system_name"],
            "zodiac_type": s["zodiac_type"]
        },
        "aspects": chart["chart_data"]["aspects"],
        "elements": chart["chart_data"]["elements_distribution"],
        "qualities": chart["chart_data"]["qualities_distribution"],
        "ai_context": context["context"]
    })

JavaScript (Express Example) #

import express from "express";

const app = express();
app.use(express.json());

const api = new AstrologerAPI(process.env.RAPIDAPI_KEY);

app.post("/api/profile", async (req, res) => {
  try {
    const subject = {
      name: req.body.name,
      year: req.body.year,
      month: req.body.month,
      day: req.body.day,
      hour: req.body.hour,
      minute: req.body.minute,
      city: req.body.city,
      nation: req.body.nation,
      longitude: req.body.longitude,
      latitude: req.body.latitude,
      timezone: req.body.timezone
    };

    // Parallel API calls
    const [chart, context] = await Promise.all([
      api.getNatalChart(subject),
      api.getNatalContext(subject)
    ]);

    const s = chart.chart_data.subject;

    res.json({
      chartSvg: chart.chart,
      profile: {
        sun: { sign: s.sun.sign, house: s.sun.house },
        moon: { sign: s.moon.sign, house: s.moon.house },
        rising: { sign: s.ascendant.sign }
      },
      aspects: chart.chart_data.aspects,
      elements: chart.chart_data.elements_distribution,
      qualities: chart.chart_data.qualities_distribution,
      aiContext: context.context
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

Deployment Tips #

Keep Your API Key Server-Side #

Never embed your RapidAPI key in client-side JavaScript. Always proxy requests through your backend. Store the key in environment variables.

Cache Responses #

Natal chart data for a given set of birth parameters never changes. Cache these responses aggressively (e.g., in Redis or a database) keyed by the birth data hash. Transit data should be cached with a shorter TTL (hourly or daily).

Handle Rate Limits #

If the API returns a 429 status, implement exponential backoff. For high-traffic apps, pre-compute popular queries during off-peak hours.

When building a user profile page, fetch the natal chart and AI context in parallel (as shown in Step 8) rather than sequentially. This halves the total wait time.

Next Steps #

Build With Astrology Data

Natal charts, synastry, transits & AI interpretations — all via REST API.

Try Astrologer API