The architecture behind every answer
Eleven stages sit between your request and the JSON you receive. Each one is inspectable, and the last of them can show you what the others decided.
Request lifecycle
From HTTP request to explained response
This is the actual execution order. Nothing here is aspirational — every stage exists in the deployed engine.
Client
Your applicationdatelocation_id or lat/lonlangpackREST API
Routing and contractOpenAPI 3.181 endpointsJSON onlyAuthentication
Identity and entitlementX-API-Keyplan tier402 on gated endpointSwiss Ephemeris engine
AstronomyLahiri ayanamsatithi intervalssunrise / sunset / moonriseCandidate builder
Day context assemblyDayContext3-day windowkshaya / vriddhi naturalKala evaluation
Time-of-day windowsmadhyahnaaparahnapradoshanishitaFestival Engine V2
Rule evaluation9 evaluatorsTieBreakEngineoverlap in minutesRule versioning
ProvenanceDRAFT → ARCHIVEDsupersedes_rule_idreproducibleVerification framework
Continuous checkingTier A 42/42Tier B hash lockTier C 26/26Response builder
Serialisation_engine envelopeexplain trace?observatory=trueSDK
Your languageOpenAPI 3.1generate your ownofficial SDKs in progress
Layer detail
What each layer is responsible for
Client
Your application
REST API
Routing and contract
Authentication
Identity and entitlement
Swiss Ephemeris engine
Astronomy
Candidate builder
Day context assembly
Kala evaluation
Time-of-day windows
Festival Engine V2
Rule evaluation
Rule versioning
Provenance
Verification framework
Continuous checking
Response builder
Serialisation
SDK
Your language
Astronomy
Where the numbers come from
The calendar layer never invents a position. Everything downstream is a function of the ephemeris output and the location's sunrise, sunset and moonrise.
Why a three-day window
{
"date": "2026-11-08",
"tithi": {
"num": 30,
"name": "Amavasya",
"paksha": "KRISHNA",
"starts_at": "2026-11-08T11:28:00+05:30",
"ends_at": "2026-11-09T13:01:00+05:30"
},
"sunrise": "06:39:02",
"sunset": "17:31:14",
"ayanamsa": { "system": "LAHIRI", "value": 24.2216 }
}Kala evaluation
Vyapti is the whole idea
A festival rule does not ask which tithi was running at sunrise. It asks whether the target tithi prevails during a specific window of the day — and by how many minutes.
Vyapti — does Amavasya prevail during Pradosha?
Diwali 2026. Amavasya runs 08 Nov 11:28 → 09 Nov 13:01. Pradosha is ~96 minutes after sunset on each day.
On 8 November, Amavasya begins at 11:28 and is still running when Pradosha opens at sunset. It covers the entire window — 96 minutes of overlap, a full match. On 9 November, Amavasya has already ended at 13:01, hours before Pradosha begins. Overlap is zero.
The engine therefore selects 8 November with confidence 100, and records why. A sunrise-snapshot engine would have seen Amavasya running at sunrise on 9 November and chosen the wrong day — which is precisely the class of bug this architecture removes.
| Evaluator | Window | Example |
|---|---|---|
| SUNRISE_VYAPINI | Sunrise | Legacy rules; tithi containing sunrise |
| MADHYAHNA_VYAPINI | Midday (2/5–3/5 of day) | Ganesh Chaturthi |
| APARAHNA_VYAPINI | Afternoon (3/5–4/5 of day) | Vijayadashami |
| PRADOSHA_VYAPINI | Sunset + ~96 min | Diwali, Dhanteras |
| NISHITA_VYAPINI | Midnight muhurta | Maha Shivaratri, Janmashtami |
| MOONRISE | Local moonrise | Karwa Chauth, Sankashti |
| SANKRANTI | Solar ingress | Makar Sankranti |
| NAKSHATRA | Nakshatra × kala | Onam, Karthigai Deepam |
| COMBINED | AND of conditions | Tithi + nakshatra + kala |
Explainability
The trace is the product
Every evaluation emits a trace — whether the rule matched or not. This is what /v1/festivals/explain returns, and it is stored server-side for regression runs.
{
"festival": "FESTIVAL_DIWALI",
"rule_code": "DIWALI_V2",
"rule_version": 2,
"confidence": 100,
"confidence_label": "FULL_KALA_COVERAGE",
"evaluator": "PRADOSHA_VYAPINI",
"candidate_days": [
"2026-11-08",
"2026-11-09"
],
"target_tithi": "Amavasya (Kartika amanta)",
"tithi_interval": "2026-11-08T11:28+05:30 → 2026-11-09T13:01+05:30",
"kala": {
"name": "Pradosha",
"2026-11-08": "17:31–19:07",
"2026-11-09": "17:31–19:07"
},
"overlap_minutes": {
"2026-11-08": 96,
"2026-11-09": 0
},
"selected": "2026-11-08",
"reason": "Amavasya fully covers Pradosha on Nov 8; absent during Pradosha on Nov 9",
"tie_break_applied": null,
"authority": [
"Dharmasindhu",
"Nirnaya Sindhu"
]
}evaluator
which strategy ran
overlap_minutes
the evidence
confidence
100 / 60–99 / 55 / 40
authority
shastric source
Dharmasindhu, Nirnaya Sindhu. A date you can attribute is a date you can defend.rule_version
reproducibility
Observatory mode
Raw astronomical facts, on request
Adding ?observatory=true attaches the underlying intervals to each festival. It is assembled from data the pipeline already produced, so it costs no extra computation.
{
"festival": "FESTIVAL_DIWALI",
"selected_date": "2026-11-08",
"confidence": 100,
"astronomical_facts": {
"amavasya": {
"start": "2026-11-08 11:28",
"end": "2026-11-09 13:01"
},
"pradosha": { "start": "17:31", "end": "19:07" }
},
"evaluation": {
"rule": "PRADOSHA_VYAPINI",
"overlap_minutes": 96
},
"authority": ["Dharmasindhu", "Nirnaya Sindhu"]
}Observatory mode exists because a derivation you cannot check is only marginally better than no derivation at all. With the raw intervals attached, you can recompute the overlap yourself and confirm the engine reached the right conclusion.
Off by default
Rule versioning
Rules are never edited in place
A correction inserts a new row with an incremented version and a pointer to the row it supersedes. Nothing is deleted, so history stays queryable forever.
DRAFT
Nobody
Authored but invisible to the engine. Reviewable in the database.
EXPERIMENTAL
Opt-in only
Loadable via ?rules=experimental. Never served by default.
CURRENT
Production
The only status the engine loads under normal operation.
DEPRECATED
History
Superseded by a newer version. Retained for reproducibility.
ARCHIVED
History
Retired entirely. Past traces still resolve against it.
Why this matters for you
Draft rules are invisible
DRAFT and is not loaded by the engine at all. Only CURRENT rules serve production traffic; EXPERIMENTAL requires an explicit opt-in per request.Verification
Three tiers, running against every build
Reference comparison catches wrong rules. The determinism lock catches accidental drift. Audit replay catches deployment regressions.
Reference comparison
7 festivals × 6 years, spanning 1950–2100
Diwali, Janmashtami, Maha Shivaratri, Ganesh Chaturthi, Vijayadashami, Holi and Makar Sankranti are compared against DrikPanchang across a 150-year span. This catches rules that happen to be right for the current decade but wrong historically.
Determinism lock
Every-day festival hash across 8 years
A hash of every festival on every day for eight years is stored in tier_b_lock.json. Re-running the engine must reproduce it exactly. This is the guard against silent drift: change a rule for one festival and accidentally move another, and the lock breaks immediately.
Audit replay
22 days from the 2026–27 accuracy audit
Assertions taken from a manual accuracy audit are replayed against production. Where Tier A proves agreement with an external reference, Tier C proves the deployed system still behaves as audited.
Response building
What comes back, and how to read it
_engine
envelope
confidence
always present
explain
Starter and above
astronomical_facts
?observatory=true
"_engine": {
"festival_engine": "v2.0.0",
"rule_pack": "NORTH_INDIA",
"rules_snapshot_version": 14
}Read the derivation yourself
The playground issues a real request against the production API and shows you the raw response, the explanation and the code side by side.