Timezone Handling in Astrology APIs: The Bug That Breaks Every Birth Chart
The ascendant changes sign every 2 hours. A single timezone error doesn't just shift your chart -- it makes every prediction after it wrong.
The Bug Report That Ruins Your Weekend
It's Friday afternoon. A bug report comes in from a paying customer in Mumbai: "The ascendant in the birth chart is wrong. I'm getting Libra rising but my Lagna is Capricorn."
You check the logs. The user was born at 2:30 PM IST in Kolkata. Your frontend collected the local time. Your backend sent it to the astrology API. Everything looks fine. Except your code did this:
// What your frontend sent
const birthTime = new Date("1990-06-15T14:30:00");
// What actually got sent to the API (browser in US Pacific timezone)
// "1990-06-15T21:30:00Z" -- silently converted to UTC
// That's 3:00 AM IST the next day. Wrong by 12.5 hours.
The ascendant is wrong by 3 signs. The Moon has moved to a different nakshatra. The dasha calculation is off. Every house placement is shifted. The entire chart is garbage, and every AI-generated interpretation built on top of it is confidently wrong.
This is not a hypothetical scenario. This is the single most common bug in astrology software. It has been filed as a bug against every astrology app, every kundali generator, and every birth chart API since the internet started hosting them. And it keeps happening because timezone handling is genuinely hard -- harder than most developers realize until they've been burned by it.
The core problem
Birth chart calculations require the exact local time at the exact geographic location. Not UTC. Not the server's timezone. Not whatever JavaScript's Date constructor decided to do. The local clock time, at that latitude and longitude, on that specific historical date.
Why Timezone Matters More Than Any Other Input
In both Vedic and Western astrology, the birth chart is a snapshot of the sky at the moment and place of birth. The entire chart is anchored by the ascendant (Lagna in Vedic astrology) -- the zodiac sign rising on the eastern horizon at the time of birth. Every house, every planet placement, every dasha period depends on getting this one value right.
Here's the math that makes timezone errors catastrophic:
| Chart Element | Rate of Change | Error from 1-Hour Timezone Mistake |
|---|---|---|
| Ascendant (Lagna) | ~1 sign per 2 hours | Up to 15 degrees off. Can shift to adjacent sign. |
| Moon | ~13.2 degrees per day (~0.55 deg/hr) | ~0.55 degrees. Can cross nakshatra boundary. |
| House cusps | Tied to ascendant rotation | All 12 houses shift. Planet-to-house mapping breaks. |
| Vimshottari Dasha | Derived from Moon's nakshatra | Wrong nakshatra = wrong dasha sequence entirely. |
| Navamsa (D9) chart | Derived from planet degrees | Divisional charts amplify small errors. |
The ascendant moves through all 12 zodiac signs in 24 hours -- roughly one sign every 2 hours, though the rate varies by latitude and time of year. At tropical latitudes, some signs rise faster than others (signs of short ascension vs. long ascension). But the key point: a 1-hour error in time shifts the ascendant by up to 15 degrees. At a sign boundary, that flips the entire chart.
And a timezone error isn't a 1-hour error. It's often a 5.5-hour error (India), a 9-hour error (Japan), or a 12.5-hour error (when UTC conversion goes wrong across the date line). These aren't subtle bugs. They produce charts that are recognizably, completely wrong to anyone who knows their own birth chart.
Let's put concrete numbers on it. For a birth on June 15, 1990 in Mumbai (19.08N, 72.88E):
| Local Time Sent | Actual UTC | Ascendant (Lahiri) | Moon Nakshatra |
|---|---|---|---|
| 14:30 IST (correct) | 09:00 UTC | Virgo (Kanya) | Hasta |
| 14:30 treated as UTC | 14:30 UTC | Sagittarius (Dhanu) | Chitra |
| 14:30 in US Pacific | 21:30 UTC | Pisces (Meena) | Swati |
Same user input. Three completely different charts. The only difference is how the timezone was (mis)handled.
The Three Timezone Nightmares
Nightmare 1: Daylight Saving Time (DST)
DST creates two kinds of impossible situations that your code must handle:
The Gap (Spring Forward): In the US, on March 10, 2024, clocks jumped from 2:00 AM to 3:00 AM EST/EDT. The time 2:30 AM simply did not exist. If a user enters "2:30 AM" as their birth time on that date, what do you do? The local clock never showed 2:30 AM. Most timezone libraries will either throw an error or silently adjust to 3:30 AM. Either way, you need a strategy.
The Overlap (Fall Back): On November 3, 2024, clocks fell back from 2:00 AM to 1:00 AM in the US. The time 1:30 AM existed twice -- once in EDT (UTC-4) and once in EST (UTC-5). If a user says they were born at "1:30 AM" on that date, you cannot resolve the UTC offset without asking them which 1:30 AM. Was it the first one (still daylight saving) or the second one (standard time)?
This isn't academic. A 1-hour difference between EDT and EST is enough to shift the ascendant by up to 15 degrees.
// Demonstrating the DST overlap problem
// November 3, 2024 in New York — 1:30 AM happens twice
const luxon = require('luxon');
// First 1:30 AM (EDT, UTC-4)
const first = luxon.DateTime.fromObject(
{ year: 2024, month: 11, day: 3, hour: 1, minute: 30 },
{ zone: 'America/New_York' }
);
// first.offset = -240 (EDT)
// first.toUTC().toISO() = "2024-11-03T05:30:00.000Z"
// But Luxon picks the first occurrence by default.
// The second 1:30 AM (EST, UTC-5) would give:
// "2024-11-03T06:30:00.000Z"
// That's a 1-hour difference — enough to change the ascendant.
Historical DST makes it worse. India observed DST during World War II (1942-1945). If you're calculating a chart for someone born in Calcutta on September 1, 1943, the offset was UTC+6:30 (IST + 1 hour war time), not the modern UTC+5:30. Most developers don't know this. Most timezone libraries do know it -- but only if you pass an IANA timezone identifier, not a raw UTC offset.
Countries that have changed DST rules since 2000
Russia (abolished DST 2011, then switched to permanent DST, then switched back). Turkey (abolished DST 2016). Brazil (abolished DST 2019). Morocco (adopted year-round DST 2018, then reversed). Egypt (re-introduced DST 2014, abolished again 2014). The rules change constantly.
Nightmare 2: Historical Timezone Changes
Before the late 19th century, there were no standardized timezones. Each city set its clocks to local solar mean time. This means the UTC offset for a given city depends on when the birth occurred:
| Location | Period | UTC Offset |
|---|---|---|
| Kolkata (Calcutta) | Before 1880 | +05:53:28 (local mean time) |
| Kolkata | 1880-1906 | +05:53:28 (Calcutta Time) |
| All India | 1906-1941 | +05:30 (IST adopted) |
| All India | 1942-1945 | +06:30 (war time DST) |
| All India | 1945-present | +05:30 (IST) |
India adopted IST (UTC+5:30) on January 1, 1906. But IST was set based on a longitude of 82.5 degrees East (Allahabad observatory), not Kolkata's longitude of 88.37 degrees East. Before 1906, Kolkata used its own local mean time of UTC+5:53:28. That's a 23-minute difference from modern IST. Twenty-three minutes is enough to move the ascendant by almost 6 degrees -- which, at a sign boundary, flips the entire chart.
And India is relatively simple. China consolidated from five timezone zones (Kunlun, Sinkiang-Tibet, Kansu-Szechuan, Chungyuan, and Changpai) into a single UTC+8 zone in 1949. Someone born in Urumqi in 1948 used UTC+6. Today, the same city officially uses UTC+8, despite its solar noon being closer to 3 PM clock time. Xinjiang informally uses UTC+6 to this day.
Other extreme cases: Nepal uses UTC+5:45. Samoa skipped December 30, 2011 entirely when it jumped across the International Date Line from UTC-11 to UTC+13. The Chatham Islands of New Zealand use UTC+12:45. North Korea created its own timezone (UTC+8:30) in 2015, then abolished it in 2018.
Nightmare 3: IANA Timezone vs. UTC Offset
There are fundamentally two ways to express "what time was it":
IANA Timezone Identifier (e.g., Asia/Kolkata, America/New_York): This is a reference to a database of historical timezone rules maintained by ICANN. Given a date and an IANA identifier, a timezone library can resolve the correct UTC offset for that specific moment in history, including DST transitions and historical changes. It's the "correct" answer for nearly all cases.
UTC Offset (e.g., +05:30, -04:00): This is an explicit, unambiguous statement: "the local clock was this many hours and minutes ahead of (or behind) UTC." No database needed. No DST resolution. No historical lookup. But the caller must already know the correct offset for that date.
Both have tradeoffs for astrology APIs:
IANA Identifier
- Handles DST automatically
- Handles historical changes
- Requires IANA database on server
- Ambiguous for DST overlaps
- Database must stay updated
UTC Offset
- Unambiguous and explicit
- No database dependency
- Caller must resolve DST
- Caller must know historical rules
- Shifts responsibility to app layer
The Vedika API Approach: Explicit UTC Offsets
Vedika API accepts the UTC offset format: +HH:MM or -HH:MM. This is a deliberate design decision, not a limitation.
The reasoning: timezone resolution belongs at the application layer, not the calculation layer. Your UI knows whether the user selected "Eastern Daylight Time" or "Eastern Standard Time." Your frontend has access to the user's location, the IANA database, and the context to resolve ambiguity. The astrology API should not be guessing which of two possible UTC offsets is correct for a given local time.
Here's the request format:
// Vedika API V1 — birth chart request
const response = await fetch('https://api.vedika.io/api/vedika/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'vk_live_your_api_key_here'
},
body: JSON.stringify({
question: "Analyze my birth chart",
birthDetails: {
datetime: "1990-06-15T14:30:00",
latitude: 19.0760,
longitude: 72.8777,
timezone: "+05:30" // UTC offset, NOT "Asia/Kolkata"
}
})
});
Common mistake
Sending "timezone": "Asia/Kolkata" will be rejected with the error: "Timezone offset must be in format +HH:MM or -HH:MM". You must resolve the IANA identifier to a UTC offset before calling the API.
The datetime field is the local time as it appeared on the clock in that location. It is NOT UTC. The timezone field tells the API how to convert it to UTC for the astronomical calculation. Together, they unambiguously define the moment in time.
This means the API never has to guess. It never has to look up DST rules. It never has to maintain a timezone database. The calculation is deterministic: given a UTC moment and geographic coordinates, Swiss Ephemeris returns exact planetary positions.
# Vedika API V1 — Python example
import requests
response = requests.post(
'https://api.vedika.io/api/vedika/chat',
headers={
'Content-Type': 'application/json',
'x-api-key': 'vk_live_your_api_key_here'
},
json={
'question': 'Analyze my birth chart',
'birthDetails': {
'datetime': '1990-06-15T14:30:00',
'latitude': 19.0760,
'longitude': 72.8777,
'timezone': '+05:30'
}
}
)
chart = response.json()
Practical Solutions for Developers
Rule 1: Resolve Timezone at the UI Layer
Your frontend is where the user selects their birth date, time, and location. That's where timezone resolution should happen. Use a timezone library to convert the user's IANA timezone to a UTC offset for the specific birth date.
// JavaScript: Resolve IANA timezone to UTC offset using Luxon
import { DateTime } from 'luxon';
function getUtcOffset(dateStr, timeStr, ianaTimezone) {
// Parse the local time in the user's timezone
const dt = DateTime.fromISO(`${dateStr}T${timeStr}`, {
zone: ianaTimezone
});
if (!dt.isValid) {
throw new Error(`Invalid date/time: ${dt.invalidReason}`);
}
// Check if this time is in a DST gap
// Luxon automatically adjusts, but you should warn the user
const reconstructed = DateTime.fromISO(
`${dateStr}T${timeStr}`,
{ zone: ianaTimezone }
);
if (reconstructed.toFormat('HH:mm') !== timeStr) {
console.warn(
`Time ${timeStr} doesn't exist in ${ianaTimezone} on ${dateStr}. ` +
`Adjusted to ${reconstructed.toFormat('HH:mm')}.`
);
}
// Get the UTC offset in +HH:MM format
const offsetMinutes = dt.offset;
const sign = offsetMinutes >= 0 ? '+' : '-';
const absMinutes = Math.abs(offsetMinutes);
const hours = String(Math.floor(absMinutes / 60)).padStart(2, '0');
const minutes = String(absMinutes % 60).padStart(2, '0');
return `${sign}${hours}:${minutes}`;
}
// Examples:
getUtcOffset('1990-06-15', '14:30', 'Asia/Kolkata');
// Returns: "+05:30"
getUtcOffset('1990-07-15', '14:30', 'America/New_York');
// Returns: "-04:00" (EDT — daylight saving)
getUtcOffset('1990-01-15', '14:30', 'America/New_York');
// Returns: "-05:00" (EST — standard time)
getUtcOffset('1943-09-01', '14:30', 'Asia/Kolkata');
// Returns: "+06:30" (India war time DST)
# Python: Resolve IANA timezone to UTC offset
from datetime import datetime
from zoneinfo import ZoneInfo # Python 3.9+
def get_utc_offset(date_str: str, time_str: str, iana_timezone: str) -> str:
"""
Given a local date, time, and IANA timezone, return the UTC offset
in +HH:MM or -HH:MM format suitable for the Vedika API.
"""
tz = ZoneInfo(iana_timezone)
dt = datetime.strptime(f"{date_str}T{time_str}", "%Y-%m-%dT%H:%M")
dt_aware = dt.replace(tzinfo=tz)
# Get the UTC offset
offset = dt_aware.utcoffset()
total_seconds = int(offset.total_seconds())
sign = '+' if total_seconds >= 0 else '-'
total_seconds = abs(total_seconds)
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
return f"{sign}{hours:02d}:{minutes:02d}"
# Examples:
print(get_utc_offset('1990-06-15', '14:30', 'Asia/Kolkata'))
# "+05:30"
print(get_utc_offset('1990-07-15', '14:30', 'America/New_York'))
# "-04:00" (EDT)
print(get_utc_offset('1990-01-15', '14:30', 'America/New_York'))
# "-05:00" (EST)
Rule 2: Store the Original Local Time + Timezone, Never Just UTC
This is the most important data modeling rule for astrology applications. Many developers instinctively convert to UTC and store a single timestamp. This is wrong for astrology.
// WRONG: You've lost the timezone forever
const birthTimestamp = Date.UTC(1990, 5, 15, 14, 30, 0);
// If you ever need to recalculate with a different timezone
// library or after a TZ database update, you can't.
// RIGHT: Store all three pieces of information
const birthData = {
localDatetime: "1990-06-15T14:30:00", // What the clock showed
timezone: "+05:30", // The UTC offset on that date
ianaTimezone: "Asia/Kolkata", // For future re-resolution
latitude: 19.0760,
longitude: 72.8777
};
Why store the IANA timezone alongside the UTC offset? Because the IANA timezone database is updated periodically with corrections to historical data. If a correction is published, you can re-resolve the offset from the IANA identifier. If you only stored +05:30, you can never recover that context.
Store the UTC timestamp in addition to the local time, not instead of it. UTC is useful for sorting, comparison, and deduplication. But the local time is the ground truth for astrology.
Rule 3: For Historical Dates, Lean on the IANA Database
The IANA timezone database (also known as the Olson database or tz database) contains timezone rules going back to at least 1970 for most zones, and much further for many. It knows about India's war-time DST. It knows about China's timezone consolidation. It knows about Nepal's shift from UTC+5:41:16 to UTC+5:45 in 1986.
# Python: The IANA database handles historical offsets automatically
from datetime import datetime
from zoneinfo import ZoneInfo
# India during WWII — the database knows about war time DST
kolkata = ZoneInfo('Asia/Kolkata')
# Modern IST
modern = datetime(1990, 6, 15, 14, 30, tzinfo=kolkata)
print(modern.strftime('%z')) # +0530
# Before IST adoption (pre-1906): Calcutta Mean Time
# Note: Python's zoneinfo may not cover all pre-1900 offsets.
# For very old dates, you may need to compute local mean time
# from longitude: offset_hours = longitude / 15.0
# Kolkata (88.37E): 88.37 / 15 = 5.891 hours = +05:53:28
For birth dates before 1900, the IANA database becomes unreliable. At that point, you may need to compute local mean time (LMT) directly from the longitude. The formula is straightforward: divide the longitude by 15 to get the offset in hours. For Kolkata at 88.37 degrees East: 88.37 / 15 = 5.891 hours, which is +05:53:28.
// Compute Local Mean Time offset from longitude
// Useful for births before timezone standardization
function lmtOffset(longitude) {
const totalMinutes = Math.round(longitude * 4); // 1 degree = 4 minutes of time
const sign = totalMinutes >= 0 ? '+' : '-';
const abs = Math.abs(totalMinutes);
const hours = String(Math.floor(abs / 60)).padStart(2, '0');
const minutes = String(abs % 60).padStart(2, '0');
return `${sign}${hours}:${minutes}`;
}
lmtOffset(72.8777); // Mumbai: "+04:52" (Bombay Time before 1955)
lmtOffset(88.3639); // Kolkata: "+05:53" (Calcutta Time before 1906)
lmtOffset(-73.9857); // New York: "-04:56" (before US standard time 1883)
lmtOffset(116.4074); // Beijing: "+07:46" (before China unified 1949)
Rule 4: Build a Complete Timezone Resolution Pipeline
Here's a production-ready function that takes birth data from your UI and prepares it for the Vedika API:
import { DateTime } from 'luxon';
/**
* Resolve birth time into a Vedika API-compatible request.
*
* @param {string} date - Birth date, "YYYY-MM-DD"
* @param {string} time - Birth time, "HH:mm"
* @param {string} ianaTimezone - IANA timezone, e.g. "Asia/Kolkata"
* @param {number} lat - Latitude
* @param {number} lng - Longitude
* @returns {object} Ready-to-send birthDetails object
*/
function resolveBirthDetails(date, time, ianaTimezone, lat, lng) {
const dt = DateTime.fromISO(`${date}T${time}`, {
zone: ianaTimezone
});
if (!dt.isValid) {
throw new Error(
`Cannot resolve time: ${dt.invalidReason}. ` +
`This may be a DST gap — the time ${time} may not exist ` +
`in ${ianaTimezone} on ${date}.`
);
}
// Format offset as +HH:MM or -HH:MM
const offsetMinutes = dt.offset;
const sign = offsetMinutes >= 0 ? '+' : '-';
const absMin = Math.abs(offsetMinutes);
const hh = String(Math.floor(absMin / 60)).padStart(2, '0');
const mm = String(absMin % 60).padStart(2, '0');
const utcOffset = `${sign}${hh}:${mm}`;
return {
datetime: `${date}T${time}:00`,
latitude: lat,
longitude: lng,
timezone: utcOffset
};
}
// Usage:
const birthDetails = resolveBirthDetails(
'1990-06-15',
'14:30',
'Asia/Kolkata',
19.0760,
72.8777
);
// Send to Vedika API
const response = await fetch('https://api.vedika.io/api/vedika/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'vk_live_your_api_key_here'
},
body: JSON.stringify({
question: 'Analyze my birth chart',
birthDetails: birthDetails
})
});
# Python: Complete timezone resolution pipeline
from datetime import datetime
from zoneinfo import ZoneInfo
import requests
def resolve_birth_details(
date_str: str,
time_str: str,
iana_timezone: str,
latitude: float,
longitude: float
) -> dict:
"""
Resolve birth data into a Vedika API-compatible request format.
Args:
date_str: Birth date "YYYY-MM-DD"
time_str: Birth time "HH:MM"
iana_timezone: IANA timezone identifier
latitude: Birth location latitude
longitude: Birth location longitude
Returns:
dict with datetime, latitude, longitude, timezone fields
"""
tz = ZoneInfo(iana_timezone)
naive_dt = datetime.strptime(f"{date_str}T{time_str}", "%Y-%m-%dT%H:%M")
aware_dt = naive_dt.replace(tzinfo=tz)
# Get UTC offset
offset = aware_dt.utcoffset()
total_seconds = int(offset.total_seconds())
sign = '+' if total_seconds >= 0 else '-'
abs_seconds = abs(total_seconds)
hours = abs_seconds // 3600
minutes = (abs_seconds % 3600) // 60
utc_offset = f"{sign}{hours:02d}:{minutes:02d}"
return {
'datetime': f"{date_str}T{time_str}:00",
'latitude': latitude,
'longitude': longitude,
'timezone': utc_offset
}
# Example: Send to Vedika API
birth_details = resolve_birth_details(
'1990-06-15',
'14:30',
'Asia/Kolkata',
19.0760,
72.8777
)
response = requests.post(
'https://api.vedika.io/api/vedika/chat',
headers={
'Content-Type': 'application/json',
'x-api-key': 'vk_live_your_api_key_here'
},
json={
'question': 'Analyze my birth chart',
'birthDetails': birth_details
}
)
print(response.json())
Edge Cases That Will Break Your App
Born at Midnight on a DST Transition Day
If a user was born at exactly 12:00 AM on March 10, 2024 in New York, the DST transition hasn't happened yet (it happens at 2:00 AM). The offset is EST (-05:00), not EDT (-04:00). But if your code uses the date (March 10) to decide "it's DST season," you might incorrectly apply EDT. Always resolve DST based on the exact date and time, not just the date.
Countries That Changed Their Timezone
Samoa (December 29-30, 2011): Samoa skipped December 30, 2011. They went from Thursday, December 29 directly to Saturday, December 31, jumping from UTC-11 to UTC+13. Nobody was born on December 30, 2011 in Samoa. If your date picker allows selecting that date with a Samoan timezone, your app has a data integrity issue.
India (August 15, 1947): Before independence, Indian cities used a mix of local mean times, railway time, and Calcutta Time. IST was formally adopted everywhere on independence. For births before this date, the correct offset depends on which city's time the hospital or family was following.
North Korea (2015-2018): North Korea created Pyongyang Time (UTC+8:30) on August 15, 2015, then abolished it on May 5, 2018, reverting to UTC+9 (Korean Standard Time). For births in that 3-year window, the offset is +08:30, not +09:00.
# Python: Verifying Samoa's date skip
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
samoa = ZoneInfo('Pacific/Apia')
dec_29 = datetime(2011, 12, 29, 23, 59, tzinfo=samoa)
next_minute = dec_29 + timedelta(minutes=1)
print(dec_29.strftime('%Y-%m-%d %H:%M %Z'))
# 2011-12-29 23:59 -11
print(next_minute.strftime('%Y-%m-%d %H:%M %Z'))
# 2011-12-31 00:00 +13
# December 30 never happened
Users Who Don't Know Their Exact Birth Time
This is surprisingly common, especially in developing countries where birth certificates may record only the date, not the time. In India, many older birth certificates say "morning" or "afternoon" rather than an exact time.
The impact of birth time uncertainty depends on how close the ascendant is to a sign boundary:
| Time Uncertainty | Ascendant Shift | Practical Impact |
|---|---|---|
| +/- 5 minutes | ~2.5 degrees | Safe unless very close to sign boundary |
| +/- 15 minutes | ~7.5 degrees | Could cross sign boundary. Navamsa changes. |
| +/- 30 minutes | ~15 degrees | High chance of wrong ascendant sign |
| +/- 2 hours | ~1 full sign | Almost certainly wrong ascendant |
| "Morning" (~4 hrs) | ~2 full signs | Chart is essentially a guess |
Your application should communicate this uncertainty to users. If the birth time is approximate, the chart is approximate. Some astrology applications implement "birth time rectification" -- using known life events to work backwards to a likely birth time. But that's a complex feature beyond timezone handling.
Half-Hour and Quarter-Hour Offsets
Not all UTC offsets are whole hours. Your timezone parser must handle these:
| Country | UTC Offset |
|---|---|
| India | +05:30 |
| Nepal | +05:45 |
| Iran | +03:30 |
| Myanmar | +06:30 |
| Chatham Islands (NZ) | +12:45 |
| Marquesas Islands | -09:30 |
If your timezone input only accepts whole-hour offsets (a dropdown from -12 to +12), you've already broken charts for over 1.5 billion people in India and the other half-hour-offset countries. Always accept +HH:MM format, not just +HH.
Testing Your Timezone Logic
Build a test matrix of known birth charts with verified ascendants. This is your regression suite. Every time you change timezone handling code, run these tests.
// Test matrix for timezone validation
const testCases = [
{
name: "Modern India (IST)",
date: "1990-06-15",
time: "14:30",
iana: "Asia/Kolkata",
expectedOffset: "+05:30",
lat: 19.0760,
lng: 72.8777,
expectedAscendant: "Virgo" // Verify against Swiss Ephemeris
},
{
name: "US East Coast Summer (EDT)",
date: "1990-07-15",
time: "10:00",
iana: "America/New_York",
expectedOffset: "-04:00",
lat: 40.7128,
lng: -74.0060,
expectedAscendant: "Virgo"
},
{
name: "US East Coast Winter (EST)",
date: "1990-01-15",
time: "10:00",
iana: "America/New_York",
expectedOffset: "-05:00",
lat: 40.7128,
lng: -74.0060,
expectedAscendant: "Aquarius"
},
{
name: "Japan (no DST ever)",
date: "1985-03-20",
time: "06:00",
iana: "Asia/Tokyo",
expectedOffset: "+09:00",
lat: 35.6762,
lng: 139.6503,
expectedAscendant: "Pisces"
},
{
name: "Nepal (UTC+5:45)",
date: "1995-08-10",
time: "12:00",
iana: "Asia/Kathmandu",
expectedOffset: "+05:45",
lat: 27.7172,
lng: 85.3240,
expectedAscendant: "Libra"
},
{
name: "DST gap — time doesn't exist",
date: "2024-03-10",
time: "02:30",
iana: "America/New_York",
expectedOffset: null, // Should throw or adjust
shouldError: true
},
{
name: "DST overlap — ambiguous time",
date: "2024-11-03",
time: "01:30",
iana: "America/New_York",
expectedOffset: "-04:00", // Luxon picks first occurrence (EDT)
note: "Could also be -05:00 (EST). App should ask user."
}
];
// Run the tests
for (const tc of testCases) {
try {
const offset = getUtcOffset(tc.date, tc.time, tc.iana);
if (tc.shouldError) {
console.error(`FAIL: ${tc.name} — expected error, got ${offset}`);
} else if (offset !== tc.expectedOffset) {
console.error(
`FAIL: ${tc.name} — expected ${tc.expectedOffset}, got ${offset}`
);
} else {
console.log(`PASS: ${tc.name} — ${offset}`);
}
} catch (e) {
if (tc.shouldError) {
console.log(`PASS: ${tc.name} — correctly threw: ${e.message}`);
} else {
console.error(`FAIL: ${tc.name} — unexpected error: ${e.message}`);
}
}
}
For the ascendant verification, compare your results against the Swiss Ephemeris command-line tool (swetest) or another known-good implementation. Swiss Ephemeris is the gold standard for planetary position accuracy (0.001 arcsecond). If your chart doesn't match Swiss Ephemeris, the bug is almost certainly in your timezone handling, not in the ephemeris.
# Python: Automated test runner for timezone resolution
import unittest
from datetime import datetime
from zoneinfo import ZoneInfo
class TestTimezoneResolution(unittest.TestCase):
def test_india_modern(self):
offset = get_utc_offset('1990-06-15', '14:30', 'Asia/Kolkata')
self.assertEqual(offset, '+05:30')
def test_us_summer_edt(self):
offset = get_utc_offset('1990-07-15', '10:00', 'America/New_York')
self.assertEqual(offset, '-04:00')
def test_us_winter_est(self):
offset = get_utc_offset('1990-01-15', '10:00', 'America/New_York')
self.assertEqual(offset, '-05:00')
def test_nepal_quarter_hour(self):
offset = get_utc_offset('1995-08-10', '12:00', 'Asia/Kathmandu')
self.assertEqual(offset, '+05:45')
def test_japan_no_dst(self):
offset = get_utc_offset('1985-03-20', '06:00', 'Asia/Tokyo')
self.assertEqual(offset, '+09:00')
def test_uk_summer_bst(self):
offset = get_utc_offset('1990-07-15', '12:00', 'Europe/London')
self.assertEqual(offset, '+01:00')
def test_uk_winter_gmt(self):
offset = get_utc_offset('1990-01-15', '12:00', 'Europe/London')
self.assertEqual(offset, '+00:00')
def test_iran_half_hour(self):
offset = get_utc_offset('2000-01-15', '12:00', 'Asia/Tehran')
self.assertEqual(offset, '+03:30')
if __name__ == '__main__':
unittest.main()
Common Gotchas for Developers
JavaScript Date Is a Minefield
The JavaScript Date constructor interprets strings inconsistently, and the behavior differs across browsers and Node.js versions:
// GOTCHA 1: Date-only strings are parsed as UTC
new Date("1990-06-15");
// In V8 (Chrome/Node): 1990-06-15T00:00:00.000Z (UTC midnight)
// NOT local midnight!
// GOTCHA 2: DateTime strings are parsed as local time (usually)
new Date("1990-06-15T14:30:00");
// In most engines: local time (depends on the machine's timezone)
// In some older engines: UTC
// This inconsistency is why you should NEVER use Date() for parsing
// GOTCHA 3: The month is 0-indexed in the constructor
new Date(1990, 5, 15);
// June 15, not May 15. Because months are 0-11.
// GOTCHA 4: toISOString() always returns UTC
const d = new Date("1990-06-15T14:30:00");
d.toISOString();
// "1990-06-15T18:30:00.000Z" if your machine is in EDT (-04:00)
// The local time information is GONE
// THE FIX: Use Luxon, date-fns, or Temporal (when available)
import { DateTime } from 'luxon';
const dt = DateTime.fromISO("1990-06-15T14:30:00", {
zone: 'Asia/Kolkata'
});
// dt.offset === 330 (minutes)
// dt.toUTC().toISO() === "1990-06-15T09:00:00.000Z"
// Predictable. Explicit. Correct.
Python datetime Gotchas
# GOTCHA 1: datetime.now() vs datetime.utcnow() — both are naive
from datetime import datetime
datetime.now() # Naive datetime in local timezone (no tzinfo)
datetime.utcnow() # Naive datetime in UTC (still no tzinfo!)
# Both are "naive" — neither carries timezone information.
# Comparing them or using them for timezone math is WRONG.
# GOTCHA 2: .replace(tzinfo=...) vs .astimezone()
from zoneinfo import ZoneInfo
naive = datetime(1990, 6, 15, 14, 30)
# .replace() ATTACHES a timezone without converting
aware = naive.replace(tzinfo=ZoneInfo('Asia/Kolkata'))
# aware is 14:30 IST — this is what you want for birth time
# It means "the clock showed 14:30 in Kolkata"
# .astimezone() CONVERTS to a new timezone
utc_wrong = naive.astimezone(ZoneInfo('Asia/Kolkata'))
# This first assumes naive is in the SYSTEM timezone,
# then converts to IST. Almost certainly wrong.
# THE FIX: Always use .replace() for birth times
birth_time = datetime(1990, 6, 15, 14, 30)
birth_aware = birth_time.replace(tzinfo=ZoneInfo('Asia/Kolkata'))
birth_utc = birth_aware.astimezone(ZoneInfo('UTC'))
# birth_utc is 09:00 UTC — correct
Unix Timestamps Lose Timezone Context
A Unix timestamp (seconds since 1970-01-01T00:00:00Z) unambiguously identifies a moment in time. So why is it wrong for astrology?
// Storing birth time as Unix timestamp
const birthUnix = 645451800; // 1990-06-15T09:00:00Z
// Problem: you can never recover what the local clock showed.
// Was this 14:30 IST? 11:00 CEST? 05:00 EDT?
// All three are different people, born at different local times,
// in different timezones. The birth charts are completely different.
// If a user later says "I was actually born at 14:45, not 14:30"
// and they're in IST, you need to add 15 minutes. But 15 minutes
// to what? You don't know the original timezone context.
// If you add 15 minutes to the UTC timestamp, you get the right
// answer — this time. But if the user says "actually I was in
// EDT that day, not IST," you'd need the original local time
// to recompute, and you don't have it.
Store the local time, the timezone, and the latitude/longitude as separate fields. Compute the Unix timestamp if you need it, but never use it as the primary storage format for birth data.
Server Timezone Contamination
If your server runs in UTC (most cloud environments), the code may work fine. If it runs in a developer's local timezone during testing and UTC in production, you'll get different results. If it runs in US-East and you have Indian users, every chart will be silently wrong.
// DANGEROUS: Server timezone leaks into calculation
const birthTime = new Date("1990-06-15T14:30:00");
// On a UTC server: interpreted as UTC → wrong for IST births
// On an IST server: interpreted as IST → accidentally correct
// On a PST server: interpreted as PST → wrong for everyone
// SAFE: Always be explicit about timezone
import { DateTime } from 'luxon';
const birthTime = DateTime.fromISO("1990-06-15T14:30:00", {
zone: "Asia/Kolkata" // Explicit. Server timezone is irrelevant.
});
Set TZ=UTC in your server environment to make the behavior consistent. But more importantly, never rely on the server's timezone for anything. Always be explicit.
The Vedika API Rejection You'll Hit
If you send an IANA timezone string to the Vedika API instead of a UTC offset, you'll get this error:
// This will be REJECTED
{
"birthDetails": {
"datetime": "1990-06-15T14:30:00",
"latitude": 19.0760,
"longitude": 72.8777,
"timezone": "Asia/Kolkata" // WRONG FORMAT
}
}
// Error response:
{
"error": "Timezone offset must be in format +HH:MM or -HH:MM"
}
// This is CORRECT
{
"birthDetails": {
"datetime": "1990-06-15T14:30:00",
"latitude": 19.0760,
"longitude": 72.8777,
"timezone": "+05:30" // CORRECT FORMAT
}
}
This is by design. The API doesn't maintain an IANA database because it would add a dependency that could drift, become stale, or disagree with the caller's IANA version. The explicit UTC offset eliminates an entire class of bugs.
Complete Integration Example
Here's a full working example that takes user input from a form, resolves the timezone, and calls the Vedika API:
// Complete client-side integration
import { DateTime } from 'luxon';
class BirthChartClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.vedika.io';
}
/**
* Resolve timezone and call Vedika API for birth chart analysis.
*
* @param {Object} input - User input from the form
* @param {string} input.date - "YYYY-MM-DD"
* @param {string} input.time - "HH:mm"
* @param {string} input.timezone - IANA timezone (e.g., "Asia/Kolkata")
* @param {number} input.latitude
* @param {number} input.longitude
* @param {string} input.question - What to analyze
*/
async getChart(input) {
// Step 1: Resolve IANA timezone to UTC offset for this date
const dt = DateTime.fromISO(
`${input.date}T${input.time}`,
{ zone: input.timezone }
);
if (!dt.isValid) {
throw new Error(
`Invalid date/time in ${input.timezone}: ${dt.invalidReason}. ` +
`Hint: This time may not exist due to DST.`
);
}
// Step 2: Format the UTC offset
const offsetMin = dt.offset;
const sign = offsetMin >= 0 ? '+' : '-';
const absMin = Math.abs(offsetMin);
const hh = String(Math.floor(absMin / 60)).padStart(2, '0');
const mm = String(absMin % 60).padStart(2, '0');
const utcOffset = `${sign}${hh}:${mm}`;
// Step 3: Call the API
const response = await fetch(`${this.baseUrl}/api/vedika/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey
},
body: JSON.stringify({
question: input.question,
birthDetails: {
datetime: `${input.date}T${input.time}:00`,
latitude: input.latitude,
longitude: input.longitude,
timezone: utcOffset
}
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(
`API error ${response.status}: ${error.error || error.message}`
);
}
return response.json();
}
}
// Usage
const client = new BirthChartClient('vk_live_your_key_here');
const chart = await client.getChart({
date: '1990-06-15',
time: '14:30',
timezone: 'Asia/Kolkata', // IANA — resolved to +05:30 automatically
latitude: 19.0760,
longitude: 72.8777,
question: 'Analyze my birth chart with planetary positions and dashas'
});
console.log(chart);
# Complete Python integration
from datetime import datetime
from zoneinfo import ZoneInfo
import requests
class BirthChartClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = 'https://api.vedika.io'
def _resolve_offset(self, date_str: str, time_str: str,
iana_tz: str) -> str:
"""Convert IANA timezone to UTC offset for a specific date/time."""
tz = ZoneInfo(iana_tz)
naive = datetime.strptime(f"{date_str}T{time_str}", "%Y-%m-%dT%H:%M")
aware = naive.replace(tzinfo=tz)
offset = aware.utcoffset()
total = int(offset.total_seconds())
sign = '+' if total >= 0 else '-'
total = abs(total)
return f"{sign}{total // 3600:02d}:{(total % 3600) // 60:02d}"
def get_chart(self, date: str, time: str, timezone: str,
latitude: float, longitude: float,
question: str = 'Analyze my birth chart') -> dict:
"""
Get a birth chart analysis from the Vedika API.
Args:
date: Birth date "YYYY-MM-DD"
time: Birth time "HH:MM"
timezone: IANA timezone (e.g., "Asia/Kolkata")
latitude: Birth location latitude
longitude: Birth location longitude
question: Analysis question
Returns:
API response as dict
"""
utc_offset = self._resolve_offset(date, time, timezone)
response = requests.post(
f'{self.base_url}/api/vedika/chat',
headers={
'Content-Type': 'application/json',
'x-api-key': self.api_key
},
json={
'question': question,
'birthDetails': {
'datetime': f'{date}T{time}:00',
'latitude': latitude,
'longitude': longitude,
'timezone': utc_offset
}
}
)
response.raise_for_status()
return response.json()
# Usage
client = BirthChartClient('vk_live_your_key_here')
chart = client.get_chart(
date='1990-06-15',
time='14:30',
timezone='Asia/Kolkata',
latitude=19.0760,
longitude=72.8777,
question='Analyze my birth chart with planetary positions and dashas'
)
print(chart)
Timezone Handling Checklist
Before shipping your astrology app, verify each of these:
- 1. You never pass a bare
Dateobject or UTC timestamp to an astrology API. You send the local time + UTC offset. - 2. Your timezone dropdown (or auto-detection) uses IANA identifiers, which you resolve to UTC offsets at the moment of API call.
- 3. You handle DST gaps (spring forward) with an error or user prompt, not silent adjustment.
- 4. You handle DST overlaps (fall back) by asking the user which occurrence they mean, or documenting your default.
- 5. Your database stores: local datetime, UTC offset, IANA timezone, latitude, longitude -- all five fields.
- 6. Your timezone input accepts
+HH:MMformat (not just whole hours) to support India, Nepal, Iran, Myanmar. - 7. You have a test matrix of known birth charts across different timezones, including edge cases.
- 8. Your server's own timezone (
TZenv var) does not affect calculations. - 9. For pre-1900 births, you either use local mean time from longitude or clearly document the limitation.
- 10. You never use JavaScript
new Date(string)for parsing birth times. You use Luxon, date-fns-tz, or the Temporal API.
Build With Swiss Ephemeris Precision
Vedika API handles the astronomical calculations with Swiss Ephemeris accuracy. You handle the timezone resolution at your application layer -- where you have the context to get it right. 120+ endpoints for Vedic astrology, Western astrology, panchang, compatibility, and more.
Send the correct UTC offset. We'll give you the correct chart.
Get API Key