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

Guides

Timezones, DST, and historical birth data

Every chart you compute depends on one number. That number is the tzone UTC offset that was in force at the birth moment and place. Send the wrong offset and the whole chart shifts.

Most bugs here come from the user's current offset, or from a fixed offset that ignores daylight saving. Send the offset that actually applied on the birth date.

What tzone actually means

tzone is a float. The value is the UTC offset in hours at the birth moment and place. It is not a timezone name like Asia/Kolkata. It is also not the offset where your user happens to be sitting today.

India uses a single offset. A birth in India takes tzone: 5.5.

The United States observes daylight saving, so the offset for one place changes across the year. A birth in New York in July takes tzone: -4.0. A birth in New York in December takes tzone: -5.0.

The offset feeds the astronomy directly. Every planet's fullDegree, sign, nakshatra, and house in a planets response derives from the exact birth moment in UTC.

The API computes that UTC moment from the local time and tzone you send. A wrong offset shifts that moment, and shifts where every planet lands. The shift is often enough to move a planet into a different sign or house.

FieldTypeRequiredDescription
tzonefloatYesTimezone, eg: 5.5

A full planets request looks like this. The same body works for astro_details. Every field is required.

POST https://json.astrologyapi.com/v1/planets

{
  "day": 10,
  "month": 5,
  "year": 1990,
  "hour": 19,
  "min": 55,
  "lat": 19.2056,
  "lon": 25.2056,
  "tzone": 5.5
}

Where this goes wrong

The most common failure is daylight saving. Suppose a user was born in New York. Their browser reports its current offset, so a summer session reports -4 and a winter session reports -5.

If the birth happened in December but the user opens your app in July, a browser-derived offset sends -4 when the birth needed -5. The chart is off by a full hour.

The offset for a given place can differ from today's offset. Timezone rules change over time. The offset that applied on a birth date decades ago is not always the offset that place uses now.

Resolve the offset for the birth date, not for today. A one-hour error is large. As a rough guide, the ascendant moves about one degree for every four minutes of clock time.

The ascendant is the zodiac degree rising over the eastern horizon. Earth turns roughly 360 degrees in about 24 hours, which is approximately one degree every four minutes.

The actual rate varies with latitude and time of year, so treat it as an approximation. Even so, a small tzone error shifts the ascendant by degrees. That shift can change the rising sign and every house cusp.

The fix: resolve tzone from place and date, not from the browser

Resolve the offset on the server in three steps. Geocode the place, resolve the offset for the birth date, then compute the chart.

  1. Call geo_details with the place name to get latitude and longitude.
  2. Call timezone_with_dst with that lat/lon and the birth date to get the offset in force on that date.
  3. Call planets with the coordinates and the resolved tzone.

geo_details takes a place string and returns matches. One quirk matters: latitude comes back as a number, but longitude comes back as a string, such as "72.88261".

Run the longitude through parseFloat() before you pass it to any endpoint that expects a float.

POST https://json.astrologyapi.com/v1/geo_details

{
  "place": "mum",
  "maxRows": 2
}

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

timezone_with_dst resolves the offset for a lat/lon on a given calendar date. Pass the birth date, not today's date.

The date param is optional, is a string, and the API describes its format as mm-dd-yyyy. The response returns timezone as the float offset you feed into planets.

POST https://json.astrologyapi.com/v1/timezone_with_dst

{
  "latitude": 25.7464,
  "longitude": 82.6837,
  "date": "06-27-2000"
}

Response:
{
  "status": true,
  "timezone": 5.5,
  "timezone_in_ms": 19800000,
  "date": "1992-12-01T00:00:00.000Z"
}
The API rejects requests from browsers (CORS), so route every call through your server. Never put your API key in client-side code. If you need to call from a client, use a short-lived access token instead. See the access token usage guide.

Complete example

This runs on Node 18 or later with the global fetch, no packages. Set USER_ID and API_KEY from your dashboard.

The script takes a place name plus a birth date and time. It then geocodes the place, parses the string longitude, resolves the offset for the birth date, and requests each planet's position.

// Node 18+ (global fetch). No external packages.
// USER_ID and API_KEY come from your AstrologyAPI dashboard.
const USER_ID = 'USER_ID'
const API_KEY = 'API_KEY'

const BASE_URL = 'https://json.astrologyapi.com/v1'
const AUTH =
  'Basic ' + Buffer.from(USER_ID + ':' + API_KEY).toString('base64')

async function post(endpoint, body) {
  const res = await fetch(BASE_URL + '/' + endpoint, {
    method: 'POST',
    headers: {
      Authorization: AUTH,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  })
  if (!res.ok) {
    throw new Error(endpoint + ' failed: ' + res.status + ' ' + res.statusText)
  }
  return res.json()
}

async function getPlanetPositions(input) {
  // 1. Geocode the place name to coordinates.
  const geo = await post('geo_details', {
    place: input.place,
    maxRows: 1,
  })
  const match = geo.geonames && geo.geonames[0]
  if (!match) {
    throw new Error('No location found for: ' + input.place)
  }

  // geo_details returns latitude as a number but longitude as a string.
  // Parse the longitude to a float before passing it on.
  const lat = match.latitude
  const lon = parseFloat(match.longitude)

  // 2. Resolve the UTC offset in force at that place on the BIRTH date.
  const zone = await post('timezone_with_dst', {
    latitude: lat,
    longitude: lon,
    date:
      String(input.month).padStart(2, '0') +
      '-' +
      String(input.day).padStart(2, '0') +
      '-' +
      input.year,
  })
  const tzone = zone.timezone

  // 3. Get each planet's position with the resolved tzone.
  const planets = await post('planets', {
    day: input.day,
    month: input.month,
    year: input.year,
    hour: input.hour,
    min: input.min,
    lat: lat,
    lon: lon,
    tzone: tzone,
  })

  return planets
}

getPlanetPositions({
  place: 'mum',
  day: 10,
  month: 5,
  year: 1990,
  hour: 19,
  min: 55,
})
  .then((planets) => console.log(planets))
  .catch((err) => {
    console.error(err.message)
    process.exit(1)
  })

Checklist

  • Store the birth place and the resolved numeric tzone alongside the birth datetime, not just a plain datetime string.
  • Never derive the historical tzone from the browser's current Date offset. getTimezoneOffset() reflects today's rules, not the birth date's.
  • Re-resolve tzone server-side from place and date every time. Do not let users type tzone directly unless they are an astrologer who already knows it.