Kaal Jyoti APIGet a key

Time zones

More wrong answers come from this than from everything else combined, and almost all of them are one mistake: sending an instant where a wall clock belongs.

Two kinds of date, and they are not interchangeable#

A wall clock is what a birth certificate says. 1990-05-14T10:30:00 means half past ten in the morning where the birth happened — no zone attached. That is what birth.datetime is, and what date is on the panchang routes.

An instant is a moment in UTC. 2026-01-01T00:00:00Z is the same moment everywhere. That is what from, to and at are, on the window and transit routes. The Z is optional on the wire; the value means UTC either way.

If you hold a birth as a JavaScript Date or a Python datetime with a zone attached, do not serialise it and send it: convert it to the wall clock at the birth place first, and send that with the zone beside it.

Saying which zone that wall was on#

Exactly one of these, or neither:

// 1. Best: name the zone.
"birth": { "datetime": "1990-05-14T10:30:00", "timezone": "Asia/Kolkata", … }

// 2. If a zone name is the one thing you do not have.
"birth": { "datetime": "1990-05-14T10:30:00", "utc_offset": "+05:30", … }

// 3. Neither: the API derives the zone from the coordinates.
"birth": { "datetime": "1990-05-14T10:30:00", "latitude": 28.6139, "longitude": 77.209 }

Giving both is a 400. They can disagree — Asia/Kolkata with +05:00 is a contradiction, not a hint — and the API will not guess which one you meant.

Whatever route the zone took, the answer tells you what was used:

"meta": { "timezone": { "name": "Asia/Kolkata", "utc_offset": "+05:30", "source": "given" } }

source is "given" when you named a zone or an offset and "derived" when the coordinates did. name is null when you sent a bare offset — there is no zone to name.

Which to prefer#

Name the zone. timezone is resolved at that instant, so daylight saving, a zone that changed its offset, or a country that moved between zones are all handled. utc_offset is taken literally and is only right if you already did that work.

Deriving from coordinates is the convenience option: correct, slightly slower on the first call in a process (the boundary data is loaded lazily), and the right answer when all you have is a pin on a map. The zone boundaries are timezone-boundary-builder's, built from OpenStreetMap data: © OpenStreetMap contributors, available under the Open Database License (ODbL).

From a place name#

For a full place search, use a geocoder — Google Places, or any other — and send us only the latitude and longitude. Leave out timezone and utc_offset, and the API derives the zone that was in force at the birth (above). A geocoder finds every village; the time-zone history, which is the part that matters for a chart, is ours. The widgets and the WordPress plugin do exactly this when you give them your own Google Maps key (the widget documentation shows how; you pay Google under your own key, which has a free monthly allowance).

Without a geocoder, GET /v1/places is a small built-in search: it finds towns and cities of 1,000 people or more, not villages — a visitor from a village picks the nearest town, which moves a chart by very little. It turns what the visitor typed into coordinates and the zone to name:

curl "https://api.kaaljyoti.com/v1/places?q=ujjain&language=en,hi" \
  -H "Authorization: Bearer $KAALJYOTI_API_KEY"
{
  "status": "ok",
  "data": {
    "places": [
      {
        "id": 1253914,
        "name": "Ujjain",
        "names": { "en": "Ujjain", "hi": "उज्जैन" },
        "region": "Madhya Pradesh",
        "country": "IN",
        "country_name": "India",
        "latitude": 23.18239,
        "longitude": 75.77643,
        "timezone": "Asia/Kolkata",
        "population": 515215
      }
    ]
  },
  "meta": {
    "query": "ujjain",
    "language": ["en", "hi"],
    "count": 1,
    "source": "GeoNames (geonames.org), CC BY 4.0"
  }
}

It searches the places of 1,000 people or more, by the start of any word of its name, in Latin or Devanagari script, and knows the former names of the larger ones (bombay finds Mumbai, allahabad Prayagraj). country=IN narrows to one country. The best match comes first, weighed against the size of the place.

Each search costs one credit, and needs at least three characters. A place field should search from the third character and wait for a short pause in typing — two searches per form is typical; one per keystroke is not. It works with a publishable key from a page (?key=kj_pub_…), like the rest of the browser-safe routes. Place data is © GeoNames, licensed CC BY 4.0.

History is not a constant offset#

Offsets come from the platform's ICU time-zone database, which knows what a zone was actually on at a past date. The clearest example is India's war time:

FromToOffset
1 Oct 194115 May 1942+06:30
15 May 19421 Sep 1942+05:30
1 Sep 194215 Oct 1945+06:30

So a birth in Delhi in March 1944 is +06:30, not +05:30, and a chart computed at +05:30 for it is an hour wrong — which moves the ascendant by about fifteen degrees. Send timezone: "Asia/Kolkata" and you get this for free; send utc_offset: "+05:30" and you have overruled it.

You can ask directly, and it costs no credits:

curl -s "https://api.kaaljyoti.com/v1/timezone?lat=28.6139&lon=77.2090&datetime=1944-03-15T10:00:00" \
  -H "Authorization: Bearer $KAALJYOTI_API_KEY"
{
  "status": "ok",
  "data": { "name": "Asia/Kolkata", "utc_offset": "+06:30", "source": "derived" },
  "meta": { "datetime": "1944-03-15T10:00:00" }
}

Leave datetime off and you get the offset in force now.

Before about 1900#

Zones as we know them did not exist, and the database records local mean time — Calcutta was +05:53:20 until 1906, and ICU rounds such offsets to the minute. Two libraries can differ by a minute or so for these dates, which moves nothing you would notice in a sign but can move a degree. This is a property of the historical record, not of the calculation; we use the platform's database rather than a table of our own, and say so.

The panchang routes#

POST /v1/panchang, POST /v1/panchang/month and POST /v1/ephemeris/month are about a place and a day, not a person, so they take the place fields flat with no birth wrapper, and their date is a calendar date at that place:

{ "latitude": 28.6139, "longitude": 77.209, "timezone": "Asia/Kolkata", "date": "2026-09-16" }

Leave date out and you get today at that place. The day is anchored at local noon, so it is the same civil day wherever you are calling from — and an answer with no date is never cached, because "today" stops being true at midnight there.

The year's transit events#

POST /v1/transit/events lists the year's planetary events — sign ingresses, retrograde and direct stations, and on request nakshatra ingresses and combustion. They are the same for everyone, so it takes no place and no birth: only year (or a from…to window of UTC instants) and a zone to write the times in.

{ "year": 2026, "timezone": "America/New_York", "combustion": true }

Each event has its instant in UTC (time) and its local wall clock with the utc_offset_minutes in force at that moment: a year in New York is written at −05:00 until the March change, at −04:00 through the summer and at −05:00 again from November. A year runs from local midnight on 1 January to local midnight on the next. With no zone at all the times are Indian (Asia/Kolkata); utc_offset holds one offset for the whole year.

Bounds#

Years must be between 1800 and 2400. Outside that range the API refuses, with 400 validation_error.

Latitude is limited to ±89.9°. At the pole there is no ascendant to compute and the sunrise search has no answer; a polite 400 beats a 422 on every route. Between the polar circles and that limit, see why results differ for what happens to house cusps and sunrise.