Reference documentation for v1
The True Solar Time API converts a civil (clock) time and location into true solar time — the time the sundial reads. It applies the Equation of Time and the longitude correction from the timezone's standard meridian, then returns solar position, a calculation breakdown, and contextual prose explaining the history and physics behind the result.
Two input modes are supported: supply a free-text place_name (geocoded automatically) or supply latitude and longitude directly.
https://your-host/api/v1
All endpoints are under /api/v1/. Responses are always application/json.
Pass your key in the X-API-Key request header:
curl https://your-host/api/v1/solar-time \
-H "X-API-Key: tst_your_key_here" \
-H "Content-Type: application/json" \
-d '{"date":"2026-03-30","local_time":"14:30","place_name":"Singapore"}'
| Enforcement | Key supplied | Outcome |
|---|---|---|
| OFF (default) | None | Allowed — rate-limited per client IP |
| OFF (default) | Valid key | Allowed — rate-limited per key (higher limit) |
| OFF (default) | Invalid key | Allowed — falls back to IP rate limiting |
ON (API_KEY_REQUIRED=true) | None | 401 MISSING_API_KEY |
ON (API_KEY_REQUIRED=true) | Invalid key | 401 INVALID_API_KEY |
ON (API_KEY_REQUIRED=true) | Valid key | Allowed — rate-limited per key |
To request a key, see the key management page.
The API uses a fixed 60-second window. Limits vary by identity:
| Identity | Default limit |
|---|---|
| Anonymous (no key) | 60 rpm |
| API key — Free/Dev | 10 rpm |
| API key — Builder | 60 rpm |
| API key — Pro/Commercial | 120–300 rpm (set at issuance) |
When the limit is reached you receive a 429 response. Wait for the number of seconds in the Retry-After header before retrying.
Every successful response (and every 429) carries these headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Your request limit for the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until reset (429 responses only) |
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1743350400
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again in 42 second(s).",
"retry_after": 42
}
}
Canonical endpoint. Accepts a JSON body. Returns the full solar time result.
Convenience alias for the POST endpoint. Accepts the same fields as query parameters. Useful for shareable links and browser testing. Returns the identical response schema.
Exactly one mode must be used — they are mutually exclusive:
| Mode | Required fields | Notes |
|---|---|---|
| Place-name | place_name |
Geocoded automatically. Returns 409 with candidates if the name is ambiguous across countries. |
| Coordinate | latitude, longitude |
Skips geocoding. Recommended after resolving a 409. Coordinates are reverse-geocoded for display only. |
| Field | Type | Required | Description |
|---|---|---|---|
date |
string | Yes | ISO 8601 date. Example: 2026-03-30 |
local_time |
string | Yes | 24-hour local civil time. HH:MM or HH:MM:SS. Example: 14:30 |
place_name |
string | Conditional | Free-text place name. Mutually exclusive with latitude/longitude. |
latitude |
float | Conditional | Decimal degrees, −90 to +90. Must be supplied with longitude. |
longitude |
float | Conditional | Decimal degrees, −180 to +180. Must be supplied with latitude. |
A successful 200 response contains these top-level fields:
| Field | Type | Description |
|---|---|---|
true_solar_time | string | The main result — sundial time at the given location (HH:MM:SS) |
local_time | string | Civil clock input echoed back as HH:MM:SS |
utc_time | string | UTC equivalent of the input time (HH:MM:SS) |
utc_offset | string | UTC offset with DST applied (±HH:MM) |
timezone | string | IANA timezone identifier, e.g. Asia/Singapore |
dst | boolean | Whether Daylight Saving Time is in effect |
date | string | Date echoed from input (YYYY-MM-DD) |
location | object | place_name, display_name, latitude, longitude, country_code |
calculation | object | equation_of_time_minutes, longitude_correction_minutes, total_difference_minutes |
solar_position | object | azimuth (degrees, 0=N), elevation (degrees above horizon) |
confidence | string | HIGH / MEDIUM / LOW |
historical_context_status | string | found or not_found |
summary | string | Human-readable result explanation (English) |
historical_context | string | Civil-time history for this timezone (English) |
solar_context | string | Equation of Time and longitude physics explanation (English) |
validation | string | Sanity-check notes and physics bullet points |
share | object | text, url_params, url — for building shareable links |
{
"true_solar_time": "13:51:06",
"local_time": "14:30:00",
"utc_time": "06:30:00",
"utc_offset": "+08:00",
"timezone": "Asia/Singapore",
"dst": false,
"date": "2026-03-30",
"location": {
"place_name": "Singapore",
"display_name": "Singapore, Central Region, Singapore",
"latitude": 1.3521,
"longitude": 103.8198,
"country_code": "SG"
},
"calculation": {
"equation_of_time_minutes": -4.1,
"longitude_correction_minutes": -64.8,
"total_difference_minutes": -68.9
},
"solar_position": { "azimuth": 271.34, "elevation": 28.17 },
"confidence": "HIGH",
"historical_context_status": "found",
"summary": "In Singapore, the Sun is running 1 hour 8 minutes 54 seconds behind the clock.",
"historical_context": "Singapore adopted UTC+8 permanently in 1982...",
"solar_context": "The Equation of Time is -4.1 min on this date...",
"validation": " \u2713 EoT within expected range\n \u2713 Longitude correction within expected range",
"share": {
"text": "True Solar Time in Singapore on 30 Mar 2026 at 14:30 \u2192 13:51",
"url_params": "date=2026-03-30&local_time=14:30&latitude=1.3521&longitude=103.8198",
"url": "/?date=2026-03-30&local_time=14:30&latitude=1.3521&longitude=103.8198"
}
}
All errors follow this envelope:
{ "error": { "code": "MACHINE_READABLE_CODE", "message": "Human-readable description." } }
| Code | Status | Meaning |
|---|---|---|
MISSING_API_KEY | 401 | No key supplied and enforcement is on |
INVALID_API_KEY | 401 | Key not recognised or deactivated |
PLACE_NOT_FOUND | 404 | Geocoder returned no results for place_name |
AMBIGUOUS_LOCATION | 409 | Place name matched locations in multiple countries — candidates array included |
INVALID_REQUEST | 400/422 | Bad date/time format, out-of-range coordinates, or conflicting input modes |
TIMEZONE_RESOLUTION_FAILED | 422 | Coordinates resolve to international waters with no IANA timezone |
RATE_LIMIT_EXCEEDED | 429 | Too many requests — see Retry-After header |
CALCULATION_FAILED | 500 | Internal error during solar calculation |
When a place name matches locations in more than one country, the response includes a candidates array. Resolve by resubmitting in coordinate mode:
# 1. First request returns 409:
POST /api/v1/solar-time
{ "date": "2026-03-30", "local_time": "14:30", "place_name": "Springfield" }
# Response:
{ "error": { "code": "AMBIGUOUS_LOCATION", "message": "...", "candidates": [
{ "display_name": "Springfield, Illinois, US", "latitude": 39.7817, "longitude": -89.6501, "recommended": true },
...
]
}}
# 2. Resubmit with chosen candidate's coordinates:
POST /api/v1/solar-time
{ "date": "2026-03-30", "local_time": "14:30", "latitude": 39.7817, "longitude": -89.6501 }
| Code | Meaning |
|---|---|
| 200 | Calculation succeeded |
| 400 | Invalid query parameters (GET alias only) |
| 401 | Missing or invalid API key (enforcement mode only) |
| 404 | Place not found |
| 409 | Ambiguous place name — candidates provided |
| 422 | Validation failure or timezone resolution error |
| 429 | Rate limit exceeded |
| 500 | Internal calculation error |
curl -s -X POST https://your-host/api/v1/solar-time \
-H "Content-Type: application/json" \
-H "X-API-Key: tst_your_key_here" \
-d '{
"date": "2026-03-30",
"local_time": "14:30",
"place_name": "Singapore"
}'
curl -s -X POST https://your-host/api/v1/solar-time \
-H "Content-Type: application/json" \
-H "X-API-Key: tst_your_key_here" \
-d '{
"date": "2026-03-30",
"local_time": "14:30",
"latitude": 1.3521,
"longitude": 103.8198
}'
GET /api/v1/solar-time?date=2026-03-30&local_time=14:30&latitude=1.3521&longitude=103.8198
import requests
resp = requests.post(
"https://your-host/api/v1/solar-time",
headers={"X-API-Key": "tst_your_key_here"},
json={
"date": "2026-03-30",
"local_time": "14:30",
"place_name": "Singapore",
},
)
resp.raise_for_status()
data = resp.json()
print(data["true_solar_time"]) # "13:51:06"
print(data["summary"])
const resp = await fetch("https://your-host/api/v1/solar-time", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "tst_your_key_here",
},
body: JSON.stringify({
date: "2026-03-30",
local_time: "14:30",
place_name: "Singapore",
}),
});
const data = await resp.json();
console.log(data.true_solar_time); // "13:51:06"