🌍 Astrocartography API and ✋ Palmistry API are now live. Ship them in your app today.Get Started

Guides

Build a birth chart app with Next.js

This guide builds a small Next.js app. A visitor enters their birth date, time, and place. The app calls AstrologyAPI on the server.

The results view shows the ascendant and the moon sign. The same view lists each planet's sign, nakshatra, house, and retrograde status.

What you'll build

You'll create two files in your own project. A Next.js API route calls two endpoints and merges their responses. A page renders a form and shows the result.

The form posts to the API route. The route holds your credentials, so the credentials never reach the browser.

The planets endpoint returns an array with one entry per body — the Sun through Ketu, plus the ascendant. Each entry carries a zodiac sign, a nakshatra, a house, and a retrograde flag.

The astro_details endpoint adds the ascendant name and a few extra chart facts. Call both endpoints and combine the two responses into one object.

Prerequisites

  • Node.js 18 or later. The examples use the built-in fetch and Buffer, so no HTTP library is needed.
  • An AstrologyAPI user ID and API key. Get an API key if you don't have one yet.
  • A Next.js project. The API route below uses the pages-router pages/api/*.js convention. On the app router, move the same logic into a route handler.
  • Birth coordinates for the location: latitude, longitude, and the timezone offset. The quick start guide shows how to gather birth details for a request. A plain number input works for the demo below, but read the timezones guide before you let real users type a timezone offset directly.

Why the API call runs on the server

The API rejects requests from browsers. The host json.astrologyapi.com sends no CORS headers, so a fetch from browser JavaScript fails.

Your API key must also stay out of the browser. Anyone can read client-side code, so a key shipped to the browser is a leaked key. Route every call through a server and read the credentials from environment variables.

Store the credentials as ASTROLOGY_USER_ID and ASTROLOGY_API_KEY in a server-only file such as .env.local. Do not prefix them with NEXT_PUBLIC_, since that prefix exposes a value to the browser. If you need to call the API from client code instead, use an access token as shown in the access token usage guide.

The API route

Create pages/api/birth-chart.js. The route reads the birth details from the request body. The route then builds the auth header and calls both endpoints with Promise.all.

Auth is HTTP Basic: the user ID is the username, the API key is the password. If either call fails, the route returns the error status and message. A partial result never reaches the page.

// pages/api/birth-chart.js
export default async function handler(req, res) {
  if (req.method !== 'POST') {
    res.status(405).json({ error: 'Method not allowed' })
    return
  }

  const { day, month, year, hour, min, lat, lon, tzone } = req.body
  const body = { day, month, year, hour, min, lat, lon, tzone }

  const auth =
    'Basic ' +
    Buffer.from(
      `${process.env.ASTROLOGY_USER_ID}:${process.env.ASTROLOGY_API_KEY}`,
    ).toString('base64')

  const headers = {
    Authorization: auth,
    'Content-Type': 'application/json',
  }

  // Call one endpoint and throw a real error if the API rejects the request.
  async function call(endpoint) {
    const response = await fetch(
      `https://json.astrologyapi.com/v1/${endpoint}`,
      { method: 'POST', headers, body: JSON.stringify(body) },
    )
    if (!response.ok) {
      const text = await response.text()
      throw new Error(`${endpoint} failed (${response.status}): ${text}`)
    }
    return response.json()
  }

  try {
    // planets returns an array, one entry per body, each with its sign,
    // nakshatra, house, and retrograde flag. astro_details adds the
    // ascendant name and a few extra chart facts.
    const [planets, astro] = await Promise.all([
      call('planets'),
      call('astro_details'),
    ])
    res.status(200).json({ planets, ...astro })
  } catch (error) {
    res.status(502).json({ error: error.message })
  }
}

Both endpoints take the same eight fields: day, month, year, hour, min, lat, lon, and tzone. The first five are integers and the last three are floats.

planets returns an array, not a single object — one entry per body, in a fixed order from the Sun through Ketu, followed by the ascendant as a tenth entry.

One quirk worth knowing: isRetro comes back as the string "true" or "false" for the nine real planets. On the ascendant entry the value is a literal boolean, since the ascendant is never retrograde. Compare the field with planet.isRetro === 'true' rather than a truthy check.

Here is a sample planets response, trimmed to three of the ten entries:

[
  {
    "id": 0,
    "name": "Sun",
    "fullDegree": 72.18954079246434,
    "normDegree": 12.189540792464342,
    "speed": 0.9537797443255392,
    "isRetro": "false",
    "sign": "Gemini",
    "signLord": "Mercury",
    "nakshatra": "Ardra",
    "nakshatraLord": "Rahu",
    "nakshatra_pad": 2,
    "house": 9,
    "is_planet_set": false,
    "planet_awastha": "Yuva"
  },
  {
    "id": 3,
    "name": "Mercury",
    "fullDegree": 85.49171301406825,
    "normDegree": 25.491713014068253,
    "speed": -0.29467903633858694,
    "isRetro": "true",
    "sign": "Gemini",
    "signLord": "Mercury",
    "nakshatra": "Punarvasu",
    "nakshatraLord": "Jupiter",
    "nakshatra_pad": 2,
    "house": 9,
    "is_planet_set": false,
    "planet_awastha": "Mrit"
  },
  {
    "id": 9,
    "name": "Ascendant",
    "fullDegree": 207.92868587120444,
    "normDegree": 27.928685871204436,
    "speed": 0,
    "isRetro": false,
    "sign": "Libra",
    "signLord": "Venus",
    "nakshatra": "Vishakha",
    "nakshatraLord": "Jupiter",
    "nakshatra_pad": 3,
    "house": 1,
    "is_planet_set": false,
    "planet_awastha": "--"
  }
]

And a sample astro_details response (trimmed to the fields this app uses):

{
  "ascendant": "Leo",
  "sign": "Virgo",
  "SignLord": "Mercury",
  "Naksahtra": "Uttra Phalguni",
  "NaksahtraLord": "Sun",
  "Charan": 3,
  "Tithi": "Krishna Dwadashi",
  "Yog": "Vaidhriti",
  "Karan": "Kaulav",
  "tatva": "Earth",
  "name_alphabet": "Pa",
  "paya": "Silver"
}

The form and results view

Create pages/birth-chart.js. The page renders one input per birth field. It posts the form as JSON to the API route and tracks loading and error state.

The results view reads ascendant and sign from astro_details, then loops over planets to render each body's sign, nakshatra, house, and retrograde status in a table.

// pages/birth-chart.js
import { useState } from 'react'

const initialForm = {
  day: 22,
  month: 7,
  year: 1992,
  hour: 9,
  min: 21,
  lat: 25.31668,
  lon: 83.01042,
  tzone: 5.5,
}

const fields = [
  { name: 'day', label: 'Day' },
  { name: 'month', label: 'Month' },
  { name: 'year', label: 'Year' },
  { name: 'hour', label: 'Hour (24-hour)' },
  { name: 'min', label: 'Minute' },
  { name: 'lat', label: 'Latitude' },
  { name: 'lon', label: 'Longitude' },
  { name: 'tzone', label: 'Timezone offset' },
]

export default function BirthChart() {
  const [form, setForm] = useState(initialForm)
  const [result, setResult] = useState(null)
  const [error, setError] = useState('')
  const [loading, setLoading] = useState(false)

  function handleChange(event) {
    const { name, value } = event.target
    setForm((prev) => ({ ...prev, [name]: value }))
  }

  async function handleSubmit(event) {
    event.preventDefault()
    setLoading(true)
    setError('')
    setResult(null)

    try {
      const response = await fetch('/api/birth-chart', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(form),
      })
      const data = await response.json()
      if (!response.ok) {
        throw new Error(data.error || 'Request failed')
      }
      setResult(data)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

  return (
    <main>
      <h1>Birth chart</h1>

      <form onSubmit={handleSubmit}>
        {fields.map((field) => (
          <label key={field.name}>
            {field.label}
            <input
              name={field.name}
              value={form[field.name]}
              onChange={handleChange}
            />
          </label>
        ))}
        <button type="submit" disabled={loading}>
          {loading ? 'Loading' : 'Get chart'}
        </button>
      </form>

      {error && <p role="alert">{error}</p>}

      {result && (
        <>
          <dl>
            <dt>Ascendant</dt>
            <dd>{result.ascendant}</dd>
            <dt>Moon sign</dt>
            <dd>{result.sign}</dd>
          </dl>
          <table>
            <thead>
              <tr>
                <th>Planet</th>
                <th>Sign</th>
                <th>Nakshatra</th>
                <th>House</th>
                <th>Retrograde</th>
              </tr>
            </thead>
            <tbody>
              {result.planets.map((planet) => (
                <tr key={planet.id}>
                  <td>{planet.name}</td>
                  <td>{planet.sign}</td>
                  <td>{planet.nakshatra}</td>
                  <td>{planet.house}</td>
                  <td>{planet.isRetro === 'true' ? 'Yes' : 'No'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </>
      )}
    </main>
  )
}

The form starts with sample values so you can submit it once and see a real response. Add your own labels and styling as needed. The markup here stays plain on purpose.

Where to go next

To draw a chart image instead of raw fields, use the horo_chart/:chart_id endpoint. That endpoint takes the same eight birth fields plus a chart id. The response gives the twelve houses with the planets in each.

Pass D1 for the birth chart or D9 for the navamsha divisional chart.

If you're building a matchmaking feature, read the Kundli matching guide. It follows the same server-side pattern for a two-person compatibility score.

Before you take this to real users, read Credits, errors, and going to production for error handling, caching, and key hygiene.