✋ Palmistry API and 🎧 Audio Reports API are now live. Ship them in your app today.

Guides

Birth chart API in JavaScript (Node.js)

A birth chart, also called a natal chart, is a map of the sky at the moment someone was born. It shows where the Sun, Moon and planets were, seen from the birth place.

This guide shows how to get one in plain Node.js with the built-in fetch. You turn a place name into coordinates, get the UTC offset, print the Sun, Moon and rising sign, and fetch a chart image. Then you wrap it all in one module and serve it from a small Express route.

Every endpoint below is a POST to the base URL https://json.astrologyapi.com/v1 with a JSON body. Send your access token in the x-astrologyapi-key header. Plan subscribers can use HTTP Basic auth instead, with the user ID as the username and the API key as the password. We don't send CORS headers, so call the API from your server, not the browser. Building with Next.js? Follow Build a birth chart app with Next.js. Prefer Python? Read the Python version of this guide.

What a birth chart API returns

You send us a birth date, a local birth time, the latitude and longitude, and the UTC offset. We return the sign, degree and house of each planet, the house cusps (the start of each of the 12 houses) and the ascendant.

The ascendant, or rising sign, is the sign rising on the eastern horizon at birth. Western astrology uses the tropical zodiac, measured from the seasons. Vedic astrology uses the sidereal zodiac, measured from the fixed stars. We have endpoints for both.

CallZodiacWhat we return
western_horoscopeTropical (Western)Planets, houses, aspects, ascendant and midheaven in one call. Needs house_type and is_asteroids.
planets/tropicalTropical (Western)A list of planets plus an Ascendant entry. Needs house_type.
house_cusps/tropicalTropical (Western)The 12 house cusps, plus ascendant, midheaven and vertex. Needs house_type.
natal_wheel_chartTropical (Western)A chart_url for a round chart wheel image. Needs house_type, colors, chart_size and image_type.
planetsSidereal (Vedic)A list of planets with sign, house and nakshatra (lunar mansion), plus an Ascendant entry.
horo_chart_image/:chartIdSidereal (Vedic)An SVG of a Vedic chart. Use D1 for the birth chart.

Setup

You need Node.js 18 or later. It has fetch built in, so the chart code needs no packages. Express is only for the web route at the end. Then pick your credentials:

  • New accounts and wallet users: use your access token. Create it on the credentials screen of your dashboard. We read it from the x-astrologyapi-key header.
  • Plan subscribers: you can use your user ID and API key instead, sent as HTTP Basic auth.

The two options are compared in Access tokens vs API keys. Put your credentials in environment variables. Never write them into your code.

node --version   # must be 18 or later

# New accounts and wallet users: your access token
export ASTROLOGY_ACCESS_TOKEN="your-access-token"

# Plan subscribers can use the user ID and API key instead
# export ASTROLOGY_USER_ID="your-user-id"
# export ASTROLOGY_API_KEY="your-api-key"

Check that your token works with one call:

curl -s https://json.astrologyapi.com/v1/geo_details \
  -H "x-astrologyapi-key: $ASTROLOGY_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"place":"Mumbai","maxRows":1}'

With a user ID and API key, replace the token header with -u "$ASTROLOGY_USER_ID:$ASTROLOGY_API_KEY".

Put the snippets from steps 1 to 3 in one file named chart.mjs. The .mjs ending makes it an ES module, so await works at the top level. Start with this helper. It sends JSON, uses the token when it is set and Basic auth when it is not, and stops after 15 seconds:

// chart.mjs: Node 18+ has fetch built in. No packages needed.
const BASE_URL = 'https://json.astrologyapi.com/v1'

// Access token if set, otherwise the user ID and API key (HTTP Basic auth).
function authHeaders() {
  const token = process.env.ASTROLOGY_ACCESS_TOKEN
  if (token) return { 'x-astrologyapi-key': token }
  const pair = process.env.ASTROLOGY_USER_ID + ':' + process.env.ASTROLOGY_API_KEY
  return { Authorization: 'Basic ' + Buffer.from(pair).toString('base64') }
}

async function callApi(endpoint, body) {
  const res = await fetch(BASE_URL + '/' + endpoint, {
    method: 'POST',
    headers: { ...authHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(15000), // give up after 15 seconds
  })
  if (!res.ok) throw new Error(endpoint + ' failed with HTTP ' + res.status)
  return res.json()
}

Step 1: Get coordinates and the UTC offset for the birth place

Users type a city name, but the chart needs numbers. geo_details turns a place name into latitude and longitude. timezone_with_dst then gives the UTC offset at that place on the birth date, with daylight saving time (DST) included.

// Birth: 10 May 1990, 19:55 local time, Mumbai.
const geo = await callApi('geo_details', { place: 'Mumbai', maxRows: 1 })
const match = geo.geonames[0]
const lat = parseFloat(match.latitude)
const lon = parseFloat(match.longitude) // we return longitude as a string

const zone = await callApi('timezone_with_dst', {
  latitude: lat,
  longitude: lon,
  date: '05-10-1990', // the BIRTH date, as MM-DD-YYYY
})
const tzone = Number(zone.timezone) // hours from UTC, for example 5.5

console.log(match.place_name, lat, lon, tzone)

geo_details takes place and maxRows (how many matches to return). Sample response, trimmed:

{
  "geonames": [
    {
      "place_name": "Mumbai",
      "latitude": 19.07283,
      "longitude": "72.88261",
      "timezone_id": "Asia/Kolkata",
      "country_code": "IN"
    }
  ]
}

Look at the types. latitude is a number here, but longitude is a string. The code calls parseFloat() on both, so it works either way.

timezone_with_dst takes latitude, longitude and an optional date in MM-DD-YYYY format. Always send the birth date. Sample response:

{
  "status": true,
  "timezone": 5.5,
  "timezone_in_ms": 19800000,
  "date": "1992-12-01T00:00:00.000Z"
}

Read timezone. It is the offset in hours, such as 5.5 for India. Pass it as tzone in every chart call. For old dates and DST edge cases, read Timezones, DST, and birth data.

Step 2: Call the chart endpoint and print the Sun, Moon and rising sign

Western (tropical) chart

western_horoscope returns the whole Western chart in one call. It needs the eight birth fields, plus house_type (the house system) and is_asteroids.

const SIGNS = [
  'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
  'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces',
]

// Turn a longitude from 0 to 360 into a zodiac sign name.
const signOf = (longitude) => SIGNS[Math.floor(longitude / 30) % 12]

const birth = {
  day: 10, month: 5, year: 1990,
  hour: 19, min: 55,
  lat, lon, tzone,
}

const chart = await callApi('western_horoscope', {
  ...birth,
  house_type: 'placidus',
  is_asteroids: false,
})
const planets = Object.fromEntries(chart.planets.map((p) => [p.name, p]))

console.log('Sun:   ', planets.Sun.sign)
console.log('Moon:  ', planets.Moon.sign)
console.log('Rising:', signOf(chart.ascendant))

Sample response from our docs, trimmed:

{
  "planets": [
    {
      "name": "Sun",
      "full_degree": 275.6427,
      "norm_degree": 5.6427,
      "speed": 1.019,
      "is_retro": "false",
      "sign_id": 10,
      "sign": "Capricorn",
      "house": 2
    },
    {
      "name": "Moon",
      "full_degree": 12.271,
      "norm_degree": 12.271,
      "speed": 13.5085,
      "is_retro": "false",
      "sign_id": 1,
      "sign": "Aries",
      "house": 5
    }
    // ...Mars to Pluto, Node, Chiron, Part of Fortune
  ],
  "houses": [
    { "house": 1, "sign": "Sagittarius", "degree": 240.71431 }
    // ...houses 2 to 12
  ],
  "ascendant": 240.71431015862024,
  "midheaven": 156.92135925483103
  // ...vertex, lilith, aspects
}

ascendant is a longitude from 0 to 360. Each sign covers 30 degrees, starting with Aries at 0. So signOf(240.71) returns Sagittarius. With this sample, the script prints Sun Capricorn, Moon Aries and Rising Sagittarius.

Need less data? planets/tropical returns only the planet list, with the rising sign in its Ascendant entry. house_cusps/tropical returns only the houses and angles.

Vedic (sidereal) chart

For Vedic astrology, call planets with the same birth object. It needs no house_type. The rising sign, called the lagna, is the entry named Ascendant.

const vedic = await callApi('planets', birth) // no house_type here
const byName = Object.fromEntries(vedic.map((p) => [p.name, p]))

console.log('Sun:  ', byName.Sun.sign)
console.log('Moon: ', byName.Moon.sign, byName.Moon.nakshatra)
console.log('Lagna:', byName.Ascendant.sign)

Sample response, trimmed:

[
  {
    "id": 0,
    "name": "Sun",
    "fullDegree": 72.18954079246434,
    "normDegree": 12.189540792464342,
    "isRetro": "false",
    "sign": "Gemini",
    "nakshatra": "Ardra",
    "house": 9
  },
  {
    "id": 1,
    "name": "Moon",
    "fullDegree": 11.72351598586304,
    "normDegree": 11.72351598586304,
    "isRetro": "false",
    "sign": "Aries",
    "nakshatra": "Ashwini",
    "house": 7
  },
  // ...Mars to Ketu
  {
    "id": 9,
    "name": "Ascendant",
    "fullDegree": 207.92868587120444,
    "normDegree": 27.928685871204436,
    "isRetro": false,
    "sign": "Libra",
    "nakshatra": "Vishakha",
    "house": 1
  }
]

Note the field names. Western endpoints use full_degree and is_retro. This one uses fullDegree and isRetro. On planets, isRetro is the string "true" or "false". Compare it with === 'true', because the string "false" is truthy in JavaScript.

Step 3: Get a chart wheel image

For a Western wheel, call natal_wheel_chart. It takes the birth fields, house_type, and a few style fields. We draw the wheel and return a link to the image.

const wheel = await callApi('natal_wheel_chart', {
  ...birth,
  house_type: 'placidus',
  planet_icon_color: '#F57C00',
  inner_circle_background: '#FFF8E1',
  sign_icon_color: 'red',
  sign_background: '#ffffff',
  chart_size: 500,
  image_type: 'png',
})
console.log(wheel.chart_url)
{
  "status": true,
  "chart_url": "https://s3.ap-south-1.amazonaws.com/western-chart/65a7ab90-df91-11e9-9ab4-a70005bb269b.svg",
  "msg": "Chart created successfully!"
}

Show chart_url in an img tag. If you want to serve it from your own storage, download a copy with fetch.

For a Vedic chart, call horo_chart_image/D1. The chart ID goes in the URL path. D1 is the birth chart, and chartType picks the drawing style, such as north or south.

import { writeFile } from 'node:fs/promises' // put this at the top of the file

const d1 = await callApi('horo_chart_image/D1', { ...birth, chartType: 'north' })
await writeFile('chart-d1.svg', d1.svg, 'utf8')

The sample response in our docs is {svg:"SVG CODE"}. We return the SVG markup as text in the svg field, not as an image file. Save it as a .svg file or put it straight into your HTML.

Step 4: One complete module

This module chains every step into one function, getBirthChart(). It throws a clear error for missing credentials, timeouts, network errors and any response that is not HTTP 200. It also runs the chart call and the image call at the same time with Promise.all. Save it as birthChart.mjs.

// birthChart.mjs: get a natal chart from AstrologyAPI.
// Node 18+ (built-in fetch). No packages needed.
const BASE_URL = 'https://json.astrologyapi.com/v1'
const TIMEOUT_MS = 15000
const SIGNS = [
  'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo',
  'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces',
]
const WHEEL_STYLE = {
  planet_icon_color: '#F57C00',
  inner_circle_background: '#FFF8E1',
  sign_icon_color: 'red',
  sign_background: '#ffffff',
  chart_size: 500,
  image_type: 'png',
}

export class AstrologyApiError extends Error {
  constructor(message) {
    super(message)
    this.name = 'AstrologyApiError'
  }
}

export class PlaceNotFoundError extends Error {
  constructor(place) {
    super('No place found for "' + place + '"')
    this.name = 'PlaceNotFoundError'
  }
}

// Use the access token if set, otherwise the user ID and API key.
function authHeaders() {
  const token = process.env.ASTROLOGY_ACCESS_TOKEN
  if (token) return { 'x-astrologyapi-key': token }
  const userId = process.env.ASTROLOGY_USER_ID
  const apiKey = process.env.ASTROLOGY_API_KEY
  if (userId && apiKey) {
    const pair = Buffer.from(userId + ':' + apiKey).toString('base64')
    return { Authorization: 'Basic ' + pair } // HTTP Basic auth
  }
  throw new AstrologyApiError(
    'Set ASTROLOGY_ACCESS_TOKEN, or ASTROLOGY_USER_ID and ASTROLOGY_API_KEY.',
  )
}

export async function callApi(endpoint, body) {
  const headers = { ...authHeaders(), 'Content-Type': 'application/json' }
  let res
  try {
    res = await fetch(BASE_URL + '/' + endpoint, {
      method: 'POST',
      headers,
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(TIMEOUT_MS),
    })
  } catch (err) {
    if (err.name === 'TimeoutError' || err.name === 'AbortError') {
      throw new AstrologyApiError(
        endpoint + ': no response after ' + TIMEOUT_MS / 1000 + ' seconds',
      )
    }
    throw new AstrologyApiError(endpoint + ': network error: ' + err.message)
  }

  if (res.status !== 200) {
    const text = await res.text().catch(() => '')
    throw new AstrologyApiError(
      endpoint + ': HTTP ' + res.status + ': ' + text.slice(0, 300),
    )
  }
  try {
    return await res.json()
  } catch (err) {
    throw new AstrologyApiError(endpoint + ': could not read JSON: ' + err.message)
  }
}

const signOf = (longitude) => SIGNS[Math.floor(longitude / 30) % 12]
const pad2 = (n) => String(n).padStart(2, '0')
const byName = (rows) => Object.fromEntries(rows.map((p) => [p.name, p]))

export async function findPlace(place) {
  const geo = await callApi('geo_details', { place, maxRows: 1 })
  const best = geo.geonames && geo.geonames[0]
  if (!best) throw new PlaceNotFoundError(place)
  return {
    name: best.place_name,
    lat: parseFloat(best.latitude),
    lon: parseFloat(best.longitude),
  }
}

export async function utcOffset(lat, lon, day, month, year) {
  const zone = await callApi('timezone_with_dst', {
    latitude: lat,
    longitude: lon,
    date: pad2(month) + '-' + pad2(day) + '-' + year,
  })
  return Number(zone.timezone)
}

// system 'western' uses the tropical zodiac and returns chartUrl.
// system 'vedic' uses the sidereal zodiac and returns chartSvg.
export async function getBirthChart({
  day, month, year, hour, min, place,
  system = 'western',
  houseType = 'placidus',
}) {
  if (system !== 'western' && system !== 'vedic') {
    throw new RangeError("system must be 'western' or 'vedic'")
  }

  const where = await findPlace(place)
  const tzone = await utcOffset(where.lat, where.lon, day, month, year)
  const birth = { day, month, year, hour, min, lat: where.lat, lon: where.lon, tzone }
  const result = { system, place: where.name, lat: where.lat, lon: where.lon, tzone }

  let planets
  if (system === 'western') {
    const [chart, wheel] = await Promise.all([
      callApi('western_horoscope', { ...birth, house_type: houseType, is_asteroids: false }),
      callApi('natal_wheel_chart', { ...birth, house_type: houseType, ...WHEEL_STYLE }),
    ])
    if (!wheel.chart_url) {
      throw new AstrologyApiError('natal_wheel_chart: ' + (wheel.msg || 'no chart_url'))
    }
    planets = byName(chart.planets)
    result.rising = signOf(chart.ascendant)
    result.chartUrl = wheel.chart_url
  } else {
    const [rows, d1] = await Promise.all([
      callApi('planets', birth),
      callApi('horo_chart_image/D1', { ...birth, chartType: 'north' }),
    ])
    planets = byName(rows)
    result.rising = planets.Ascendant.sign
    result.chartSvg = d1.svg
  }

  result.sun = planets.Sun.sign
  result.moon = planets.Moon.sign
  return result
}

Try it from the command line with a short runner. Save this as run.mjs next to the module and run node run.mjs.

// run.mjs: node run.mjs
import { getBirthChart } from './birthChart.mjs'

try {
  const chart = await getBirthChart({
    day: 10, month: 5, year: 1990, hour: 19, min: 55,
    place: 'Mumbai',
    system: 'western',
  })
  console.log(chart.place + ' (UTC' + (chart.tzone >= 0 ? '+' : '') + chart.tzone + ')')
  console.log('Sun:    ' + chart.sun)
  console.log('Moon:   ' + chart.moon)
  console.log('Rising: ' + chart.rising)
  console.log('Wheel:  ' + chart.chartUrl)
} catch (err) {
  console.error('Error: ' + err.message)
  process.exitCode = 1
}

Pass system: 'vedic' to get the sidereal signs and a D1 chart SVG instead.

Serve it from an Express route

To use the module in a web app, put it behind a server route. Your frontend calls your route, and only your server talks to us. Install Express:

npm install express

This route checks the input, calls getBirthChart() and maps errors to clear status codes. A bad request gets 400, an unknown place gets 404, and a failed upstream call gets 502.

// server.mjs: node server.mjs
import express from 'express'
import {
  getBirthChart,
  AstrologyApiError,
  PlaceNotFoundError,
} from './birthChart.mjs'

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

app.post('/api/birth-chart', async (req, res) => {
  const { day, month, year, hour, min, place, system = 'western' } = req.body || {}

  for (const [name, value] of Object.entries({ day, month, year, hour, min })) {
    if (!Number.isInteger(value)) {
      return res.status(400).json({ error: name + ' must be a whole number' })
    }
  }
  if (typeof place !== 'string' || place.trim() === '') {
    return res.status(400).json({ error: 'place is required' })
  }
  if (system !== 'western' && system !== 'vedic') {
    return res.status(400).json({ error: "system must be 'western' or 'vedic'" })
  }

  try {
    const chart = await getBirthChart({ day, month, year, hour, min, place, system })
    return res.json(chart)
  } catch (err) {
    if (err instanceof PlaceNotFoundError) {
      return res.status(404).json({ error: err.message })
    }
    if (err instanceof AstrologyApiError) {
      console.error(err.message)
      return res.status(502).json({ error: 'Chart service failed. Try again later.' })
    }
    console.error(err)
    return res.status(500).json({ error: 'Internal error' })
  }
})

const port = process.env.PORT || 3000
app.listen(port, () => console.log('Listening on http://localhost:' + port))

Run node server.mjs, then test it:

curl -s -X POST http://localhost:3000/api/birth-chart \
  -H "Content-Type: application/json" \
  -d '{"day":10,"month":5,"year":1990,"hour":19,"min":55,"place":"Mumbai","system":"western"}'

A chart for one birth never changes. Cache the result, keyed by the birth fields, so each chart uses credits only once. Retry only when you get a 5xx status. A 4xx status means the request itself is wrong, so fix it before you try again.

Common mistakes

  • Using the browser's UTC offset. new Date().getTimezoneOffset() gives the offset on the user's device today, in minutes, with the sign flipped. For India it returns -330. A birth chart needs the offset at the birth place on the birth date. Get it from timezone_with_dst.
  • Forgetting house_type. It is required on western_horoscope, planets/tropical, house_cusps/tropical and natal_wheel_chart. Send placidus if you have no preference. Other options include koch and whole_sign.
  • Calling the API from the browser. We don't send CORS headers, so a fetch from React or plain browser code is blocked. It would also expose your access token or API key. Keep every call on your Node.js server.
  • Mixing tropical and sidereal results. Don't take the Sun from planets and the ascendant from house_cusps/tropical. The two zodiacs are about 24 degrees apart, so the chart will be wrong. Pick one system per chart. The guide on why charts differ explains more.

Go further

Every new account gets 150 free credits, so you can run this module today. See exact costs in the credits calculator.

Frequently asked questions

Do I need an npm package to call the birth chart API?

No. Node.js 18 and later include fetch, and our API is plain HTTPS with JSON. The chart module in this guide has no dependencies. You only install Express if you want the web route.

Which endpoint returns a full natal chart in one call?

For a Western chart, call western_horoscope. It returns planets, houses, aspects, the ascendant and the midheaven together. For a Vedic chart, call planets. It returns each planet with its sign, house and nakshatra, plus an Ascendant entry.

Can I call the birth chart API from React or browser JavaScript?

No. We do not send CORS headers, so browsers block direct calls. Your access token or API key would also be visible to every visitor. Call us from Node.js, then send only the results to the browser. Our Next.js guide shows the same pattern with an API route.

Why do the Western and Vedic results show different signs?

Western endpoints use the tropical zodiac, which is measured from the seasons. Vedic endpoints use the sidereal zodiac, which is measured from the fixed stars. The two are about 24 degrees apart today, so a planet can fall in a different sign in each system.

What if the user does not know their birth time?

The Sun sign is usually safe without a time. The Moon can change sign during the day, and the rising sign changes about every two hours. Do not show a rising sign or houses without a real birth time. Our guide on unknown birth times covers the options.

How many credits does one birth chart use?

Each endpoint uses a set number of credits. Use the credits calculator for exact numbers and the pricing page for plans. Every new account starts with 150 free credits.

For AI agents

  • All calls: POST to https://json.astrologyapi.com/v1/<endpoint> with a JSON body. Auth: header x-astrologyapi-key: <access token>, or HTTP Basic with user ID and API key for plan subscribers. Server-side only. Node 18+ fetch needs no packages.
  • Place: geo_details with place, maxRows. Read geonames[0], and parseFloat() the latitude and longitude.
  • Offset: timezone_with_dst with latitude, longitude, date (MM-DD-YYYY birth date). Read timezone and send it as tzone.
  • Western: western_horoscope with the eight birth fields plus house_type and is_asteroids. Rising sign is SIGNS[Math.floor(ascendant / 30)]. Wheel image: natal_wheel_chart, read chart_url.
  • Vedic: planets with the eight birth fields. Rising sign is the entry where name === 'Ascendant'. Chart image: horo_chart_image/D1, read svg.
  • Birth fields: day, month, year, hour, min (integers) and lat, lon, tzone (numbers). Never mix tropical and sidereal results in one chart.