Reference
Liturgy
The liturgical data domain and how Gianna's public /liturgy/v1 API represents it.
Updated 2026-06-17 · https://gianna.chat/liturgy/v1
This is a large and complex domain. These notes must stay concise to be useful.
The liturgical data domain and how this API represents it. A domain model, not a record of what is built. Notes flag where a shape is a deliberate choice or limit; Boundaries list what we don't serve.
What the API provides
For a (calendar, region, date), resolves and returns:
- Day context — civil date + weekday, season (+ week), liturgical-year cycles (OF lectionary A/B/C, weekday I/II, psalter week), obligation status.
- Celebration — what the day is: name (multilingual), rank, colour, saint slug, Common categories, octave membership.
- Commemorations — other saints folded in as added prayers.
- Feasts today — every saint bearing on this date, tagged by outcome (celebrated / commemorated / omitted / arrived), so "whose feast is it?" needs no resolution parsing (§6).
- Mass(es) — propers (Introit … Postcommunion) + readings (citation + text); plural where a day has several (Christmas, vigils, Requiem).
- Office — the eight hours assembled (psalms, antiphons, hymn, chapter, canticle, collect), first/second Vespers distinguished.
- Resolution (optional) — why this office won: who was commemorated/omitted/ transferred/not-observed.
- Saints — addressable per rite (
/{calendar}/saints/{id}).
All human-readable text is a { la, en, … } map, Latin the floor. The liturgy is a
resolution: the temporale (seasons, from Easter) and sanctorale (fixed feasts)
each propose an office; precedence (occurrence within a day, concurrence at its edges)
picks the winner and the fate of the rest; the winner supplies Mass + Office, assembled
Proper → Common → Psalter.
Endpoints
Placement rule. Cross-rite resources are top-level (/calendars, /saints,
/regions); rite-scoped resources nest under /calendars/{cal}. A day returns a cheap
summary; heavy layers come either inline via ?include= (the batch case) or as
addressable sub-resources (the deep-link case) — both exist (see Includes).
| method | path | query | returns |
|---|---|---|---|
GET | /liturgy/v1 | service index — links to the collections below + openapi.json | |
GET | /liturgy/v1/openapi.json | the OpenAPI 3.1 contract (source of truth for every shape) | |
GET | /liturgy/v1/calendars | the rites — ids (ordinary/traditional/dominican), edition, hours exposed | |
GET | /liturgy/v1/calendars/{cal} | one rite's metadata | |
GET | /liturgy/v1/calendars/{cal}/days/{date} | region, include, translations | summary (below) |
GET | /liturgy/v1/calendars/{cal}/days | from,to,region,include,translations | range of summaries (collection; capped span) |
GET | /liturgy/v1/calendars/{cal}/years/{label} | region,include,translations | one liturgical year of summaries (collection); canonical {label} = YYYY-YYYY (the Advent-to-Advent span) |
GET | /liturgy/v1/calendars/{cal}/days/{date}/office | region,translations | index of the hours this day exposes |
GET | /liturgy/v1/calendars/{cal}/days/{date}/office/{hour} | region,translations | one hour (lauds…; first-vespers/second-vespers; bare vespers ⇒ second) |
GET | /liturgy/v1/calendars/{cal}/days/{date}/masses | region,translations | full Mass propers (ordered readings + chants, with text) |
GET | /liturgy/v1/calendars/{cal}/days/{date}/masses/{mass} | region,translations | one Mass (midnight/dawn/day…) |
GET | /liturgy/v1/calendars/{cal}/days/{date}/resolution | region | why this office won: winner, displaced, transfer chains (§4) |
GET | /liturgy/v1/saints | q,title,month,page | search across all rites (collection, §11) |
GET | /liturgy/v1/saints/{person} | one saint, all calendars at once (§11) | |
GET | /liturgy/v1/calendars/{cal}/saints | q,title,month,page | search within one rite (collection, §11) |
GET | /liturgy/v1/calendars/{cal}/saints/{id} | one saint's rite-scoped entry (§3) | |
GET | /liturgy/v1/regions | known regions + what each overrides (transfer/precept) | |
GET | /liturgy/v1/translations | available scripture editions per language + each language's default | |
GET | /liturgy/v1/novenas | year | the novena catalogue with each one's computed feast + suggested start (no prayer text) |
GET | /liturgy/v1/novenas/upcoming | from,days | novenas whose traditional start falls within the window, soonest first |
GET | /liturgy/v1/novenas/{slug} | year | one novena, with day-by-day prayer text when available |
Includes. ?include=masses,office,resolution,martyrology folds those heavy layers inline into a
day (or every day of a range/year), so the dominant "month of readings" view is one call,
not 1 + N. The sub-resource URLs (…/masses, …/office/{hour}, …/resolution) remain for
addressable deep-links. Without include, a day is just the summary + links.
Collections (/days?from..to, /years/{label}, both /saints searches) return an
envelope, never a bare array — so the echoed query, totals, paging, and any applied caps
surface:
{ "results": [ … ], "query": { … echoed, incl. region }, "page": { "total": 342, "next": "…cursor…" },
"warnings": [ "span capped to 90 days" ] } // warnings only when something was clamped
(Arrays inside one resource — saints.*, readings — are bounded
sub-fields, not collections; they stay bare.)
Identifiers & negotiation.
- Dates: a specific day is full ISO
YYYY-MM-DD; a recurring fixed feast isMM-DD; a moveable feast is ananchorobject (§2). A liturgical year has its own identity — the canonical{label}is the Advent-to-Advent spanYYYY-YYYY(e.g.2025-2026), removing the "starts-in vs ends-in" ambiguity of a single year. A bareYYYY(the year that begins in AdventYYYY) or any contained date 301-redirects to the canonical span, so one year = one URL. regionis an optional query refinement (not a path segment, because it's optional): omitted or unknown →general(the editio typica), lenient (never an error), echoed withregion_default_applied. Contrast{cal}— a structural segment, so a badcal404s.- Path identifiers are case-insensitive.
{cal}(ordinary/traditional/dominican— no aliases),{hour}, and{mass}all resolve regardless of case (Traditional,Vespers,MIDNIGHTall work); a genuinely unknown identifier still 404s. Leniency here only ever helps a caller. - Language is via
Accept-Language(content negotiation), not a path/param. Latin is the floor, so narrowing is purely additive and 406 never occurs. Responses sendVary: Accept-Language. - Translation is a second axis, distinct from language: within one language there are
several scripture editions (en:
douay,cpdv; la:vulgate[Clementine], with Nova Vulgata future), and a user may prefer one. Selected via the optionaltranslationsquery parameter —editions=is the canonical name (matching the response field), withtranslations=accepted as an alias — a comma list of edition ids (editions=cpdv,vulgate), at most one per language, applied only to readingtext(citations and all non-scripture text are translation-independent). Unknown/absent → the per-language default edition, lenient (never an error), echoed viaeditions(and awarningsentry on collections when a request fell back). Each resolved readingtextis tagged with theeditionthat produced it, so the answer is never ambiguous./liturgy/v1/translationslists the available editions per language; the chosen set entersVaryand theETag(below). Why a query param, notAccept-Language: the HTTP language tag selects a language, not a publisher's edition within it; overloading it (en-x-cpdv) is non-standard and uncacheable by language-aware CDNs. - Saint slugs live in two namespaces — a rite-scoped
idand a canonicalperson— so the cross-links are mandatory: a rite entry carriesperson(link up), a person'scalendars{}carriesref(link down). Neither id is a dead end.
Caching. A response is a pure function of (cal, date, region, Accept-Language, translations, data-version). The ETag incorporates all six — not just the path — so a CDN keyed on
path+Vary can't mis-serve a region/language/translation variant, and a data correction or
newly-added region table bumps data-version, changing every affected ETag (the
invalidation handle for past days, which are cached long but never immutable). If-None-Match
→ 304.
Versioning & lifecycle. /liturgy/v1 is additive within the major version: fields may be
added (the Open-questions shapes will), never removed or retyped. New editions/regions arrive
as data (§2), not new versions. A breaking change ships as /liturgy/v2; the old version is then
dated for removal via Deprecation + Sunset response headers (≥ 12 months' notice), and
deprecated fields carry a Deprecation link in the docs. Nothing in v1 is ever silently
changed.
Errors are application/problem+json (RFC 9457) with a stable machine type URI, title,
status, detail, and instance:
- 400 — malformed input (date not canonical zero-padded
YYYY-MM-DD,includevalue not in the allowed set). - 404 — unknown
{cal},{person},{id}, or an{hour}/{mass}this day doesn't expose. - 405 — a non-
GETverb on a valid path (the API is read-only); carries anAllow: GETheader. - 406 — never returned: Latin is the floor, so any
Accept-Languageis satisfiable. - 422 — a well-formed request the edition can't serve (date outside its supported span).
- 429 — rate-limited, with
Retry-After. - A bad
region, an unknowntranslationsedition, or an over-capfrom/tois never an error — it falls back / clamps, with the applied value echoed (region_default_applied/editions) and awarningsentry on collections.
Conventions.
- Media type
application/json; the canonical machine contract is the published OpenAPI 3.1 document (/liturgy/v1/openapi.json) — clients are generated from it, and it is the source of truth for every shape sketched here. - Read-only & idempotent. Every endpoint is
GET(and therefore safe + idempotent + cacheable); there are no writes, so no idempotency keys are needed. - Auth & limits. Requests carry a bearer API key (
Authorization: Bearer gianna_…), minted by a Pro household in the app (POST /api/keys, shown once; manage/revoke viaGET/DELETE /api/keys). A signed-in browser session also works, for in-app use. Unauthenticated → 401 (withWWW-Authenticate: Bearer), insufficient tier → 403. Rate limits are per-household fixed windows enforced with 429 +Retry-After(andRateLimit-Limiton the 429); front-line IP limiting is the reverse proxy's. Auth and tiering do not vary the representation, so they don't enter the cache key. - Caching headers. Every response is
ETag'd with a boundedCache-Control: public, max-age=…andstale-while-revalidate. Past dates get a long max-age (the rubrical answer is stable) but are not markedimmutable— a data-correction or a newly-addedregiontable can change even a historical resolution, andimmutablewould pin the stale copy forever. TheETag(which keys on the data version, region, and language set) is the invalidation handle; clients revalidate withIf-None-Match. Responses arepublicbecause the representation never varies by caller — but the API is paywalled, so an operator-run edge cache must require the bearer key before serving from cache (do not put an anonymous shared cache in front of/liturgy/v1). - "Today". There is no server-side "today" resource — the liturgical day follows the
caller's civil date, which a UTC server must not decide. Clients resolve their local date
and request it explicitly (
…/days/2026-06-09).Accept-Language/regionnever imply a date. ?includecost guard.includeis rejected (400) on multi-day collections for the heavy layers (office,masses) beyond a small span (e.g. > 31 days), so a year-range can't request a response-size bomb; use the per-day sub-resources for those.resolution(light) is always includable.
1. Calendars (rites)
A self-contained system — own sanctorale, precedence, propers, edition. A date resolves independently in each, so precedence is per-rite (§4), not global.
| id | rite | rubrics |
|---|---|---|
ordinary | General Roman Calendar, Ordinary Form (post-1969) | 1969 General Norms |
traditional | Traditional Roman (Extraordinary Form / "TLM") | 1960 Code of Rubrics |
dominican | Dominican Rite (OP) | OP 1962 |
Each has one stable id — ordinary, traditional, dominican; no aliases — and advertises whether it exposes an Office.
Note — one nomenclature, every surface. These three words are the names across the whole product, not just this API: the household's stored calendar preference and the app's
/api/prefs, the agent's liturgy tool, and user-facing copy all use exactlyordinary/traditional/dominican. The earlier ids and aliases (roman-ordinary,roman-1962,general,of,tlm,op) were retired in one breaking rename before the API had external consumers — they 404 here, the prefs endpoint rejects them, and migration0029rewrote stored rows. Human synonyms ("TLM", "Latin Mass", "Extraordinary Form", "Novus Ordo") are natural language: the LLM layer maps them to the canonical word; parsers accept only the three (case-insensitively).
2. Resolution inputs: rite, year, region
Temporale — moveable, anchored on Easter (→ Septuagesima, Ash Wednesday, Ascension, Pentecost, the Sunday counts; Advent anchors the other end). A function of the year.
Note — Easter is computed. By the Gregorian (Western) computus; every moveable date depends on it, so it is pinned to a named algorithm + a regression table of known dates. Eastern (Julian) computus and divergent historical reckonings are out of scope.
Sanctorale — three kinds, not just "month+day":
- fixed — month+day, year-independent (Christmas 25 Dec). The bulk.
- moveable — anchored to the temporale by a rule (Holy Family = Sunday in the Christmas
octave else 30 Dec; Sacred Heart = Friday after the Corpus Christi octave). Carries an
anchor. - moveable, rite-divergent — the anchor rule differs by rite (Christ the King = last
Sunday before Advent in OF, last Sunday of October in 1962). Anchor is a
(feast, rite)property.
{ "id": "sacred-heart", "kind": "moveable",
"anchor": { "base": "corpus-christi-octave", "offset": "friday-after" } }
// base: a named temporale point (easter | pentecost | corpus-christi | advent | epiphany |
// christmas | <-octave variants>); offset: nth-weekday-after/before | sunday-within | n-days
Note — editions diff-layered internally, only merged results ship. A version is a base table + reform layers (1568 → … → 1960) — a build technique, not an exposed axis. No year-of-rubrics selector (can't ask for "1939"); edition-selection is future (Boundaries).
Region — three rubrical effects, two modelled: which holydays transfer to a Sunday (per-conference: Epiphany/Ascension/Corpus Christi), which are of obligation (§3a). It also confers proper feasts (patrons) — not modelled (Boundaries).
Note — region is narrower than a conference's real role. Proper feasts it confers do affect precedence (a patron-Solemnity changes the day) but we model only transfer + precept. The
generaldefault is the editio typica, not "most common" — it keeps Ascension/ Epiphany/Corpus Christi on the weekday though most of the world observes the Sunday; reading transfer/obligation off the default gives the typical-edition answer, not a country's. Per-conference tables are data, added over time.
Liturgical year — starts at the 1st Sunday of Advent (a civil year spans two); the OF cycles key off it:
"liturgical_year": {
"starts": "2025-11-30",
"sunday_cycle": "C", // OF A|B|C — rolls over at ADVENT; null for 1962
"weekday_cycle": "II", // OF I|II — rolls over at 1 JAN; null for 1962
"psalter_week": 2 // OF 1–4 — null when overridden
}
Note — the two cycles roll over on different dates. Sunday cycle turns at Advent; weekday cycle (I = odd civil years) at 1 January, mid-year — one
startscan't derive both, so both are carried resolved. Cyclesnullfor 1962 (annual).psalter_weekisnullfar beyond 1962: Solemnities/Feasts take Week I, and Octaves + the final days of Advent/Holy Week override the psalter — a hint for ordinary days only, never a flat 1–4.
3. The liturgical day
The day endpoint returns the summary (the cheap core + links); Office, full Mass propers, and resolution are separate requests (the table above). The summary:
{
"calendar": "traditional", "region": "general",
"region_default_applied": true, // true only when the caller named no region (§2)
"editions": { "la": "vulgate", "en": "douay" }, // scripture edition per language, always present (§10)
"date": "2026-01-28", // CIVIL date (see note)
"weekday": "wednesday", // CIVIL weekday of the civil date
"liturgical_year": { /* §2 */ },
"season": { "id": "epiphany", "arc": "after-epiphany", "week": 3 }, // §5
"obligation": { /* §3a */ },
"celebration": { /* below — the headline, singular */ },
"saints": { // §6 — THE field for "whose feast day is it?"
"celebrated": [ { "name": {…}, "id": "…", "person": "…", "rank": {…} } ],
"commemorated": [ /* sanctoral commemorations; never the feria */ ],
"remembered": [ { "name": "Ss. Primus and Felician" }, … ] // notes via ?include=martyrology
},
"masses": [ { "id": "day", "name": {…}, "colour": {…}, "option_type": "prescribed" } ],
"links": { "office_url": "…", "masses_url": "…", "resolution_url": "…" }
}
Note —
saintsis keyed by what happened.celebrated/commemoratedare THIS calendar's answer to "whose feast is it?" (a feria is not a saint and never appears);rememberedis the Roman Martyrology — EVERY saint of the date (often 5–15), celebrated anywhere or not (vendored public-domain 1916 edition — saints canonised since are absent from it, while the calendar layer carries them; its dates follow the old calendar where they differ). Future resolution outcomes (transferred,omitted) arrive as new keys, additively.remembered[].nameis English-only (Boundaries). Names are always present; each entry's eulogynotefolds in with?include=martyrology(heavy include, span-capped likemasses/office— eulogies across a 90-day range would be most of the response's bytes). Responses are gzip-compressed when negotiated.
Note — the summary lists Mass descriptors, not a count.
masses[]carries the cheap per-Mass{ id, name, colour, option_type }(no propers — those come viamasses_urlor?include=masses). A bare count would force a second call just to learn the Mass identities, and worse:celebration.colouris the day's colour, which disagrees with a multi-Mass day where each Mass has its own (a Requiem is black while the day reads white, §7). The per-Masscolourhere is authoritative for rendering a day cell.
Note — civil date is the key; two distinct "Saturday-evening" facts. (Open.) We key on the civil date (re-keying would make "give me 2026-06-09" ambiguous) and model the evening spillover as explicit relations, kept separate: first Vespers (rubrical — the next day's Office is said this evening;
liturgical_evening.belongs_to) vs anticipated Mass (canonical — a Saturday-evening Mass fulfils the Sunday precept;anticipated_mass). The Office may be tomorrow's while the Mass said is still today's. Open: their exact shape.
3a. Obligation — region-dependent, not derivable from rank.
"obligation": {
"status": "lapsed", // yes | no | lapsed
"reason": "falls on a Monday in region X", // display
"reason_code": "lapses-monday" // null | lapses-saturday | lapses-monday | transferred | not-precept-in-region
}
Note — tri-state + machine handle. A bool can't carry the partial-precept rule (lapses on a Saturday/Monday) without flipping year-to-year;
reason_codeis the programmatic handle beside the prose. Atgeneral(the typica) the answer is the universal law: every Sunday plus the canon-1246 ten (the fixed eight and the moveable Ascension/Corpus Christi), withreasonnaming which. NOT derivable from rank — most Solemnities carry no precept. Per-region lapse/transfer (reason_code,lapsed) fills in with the region tables.
Celebration
{
"name": { "la": "S. Petri Nolasci Confessoris", "en": "St. Peter Nolasco, Confessor" },
"rank": { "label": "class-3", "precedence": 711, "cycle": "sanctorale" }, // §4
"colour": { "default": "white", "alternatives": [] }, // §5
"id": "petrus-nolascus", // RITE-SCOPED slug → /{calendar}/saints/{id}
"person": "peter-nolasco", // canonical link UP → /liturgy/v1/saints/{person} (§11) — mandatory
"titles": ["confessor"], // category SET → Common(s) (§8)
"octave": null // or { "of", "day" } (§9)
}
Note — the slug is rite-scoped;
personlinks up. The same saint can differ in rank, name, even date across rites, soididentifies this rite's celebration —personis the mandatory link up to the canonical cross-rite record (§11), whosecalendars{}links back down. The two slug namespaces never collide because each id is always paired with the other.
4. Rank, precedence, resolution
The most codified part. Two questions: occurrence (two offices, same date — who is celebrated/commemorated/omitted/transferred) and concurrence (one office's second Vespers meets the next's first Vespers — who owns it; §8).
Grades differ by rite. 1962/Dominican: four classes I–IV + commemorations + feria; feast-class and day-class (Sunday, feria, vigil, octave-day) interleave (a I-class feria outranks a I-class feast; a II-class Sunday outranks a III-class feast, then commemorated). OF: Solemnity > Feast > Memorial > Optional Memorial > Weekday (1969 table).
Note — "Sunday" is itself multi-rank. 1962: I-class Sundays (Advent/Lent/Passiontide, admit almost nothing) vs II-class (green Sundays a II-class feast can displace). OF parallels it (Advent/Lent/Easter Sundays outrank all feasts; Ordinary Sundays yield to Feasts of the Lord). A Sunday's rank carries its specific class, never one tier.
"rank": { "label": "class-1", "precedence": 910, "cycle": "temporale" }
// label = native grade; precedence = sortable (higher outranks); cycle = which PROPOSED it
Notes on
rank.
cycle= proposing cycle, not nature: a Sunday/feria istemporalethough a day-class, not a feast (that distinction lives inlabel).precedencecompares only within one resolution (one rite, one day); cross-rite comparison is meaningless — you never resolve a 1962 feast against an OF one (§1).precedencesorts; it doesn't decide. (Open.) Some precedence is conditional (a Feast of the Lord displaces an Ordinary-Time Sunday but not an Advent/Lent/Easter one) — undecidable by two integers; the engine uses the full table, the integer is only the outcome. Open: expose the rule, or only its outcome?- The day is not one scalar rank — Mass, day-hours, and the two Vespers can each resolve to different offices (concurrence), addressable separately (§7/§8).
Resolution (optional) — the decision, not just the winner:
"resolution": {
"winner": "sanctorale",
"displaced": [
{ "name": {…}, "outcome": "commemorated" },
{ "name": {…}, "outcome": "omitted" },
{ "name": {…}, "outcome": "not-observed" }, // dropped for the year
{ "name": {…}, "outcome": "transferred", "chain": ["2026-01-28","2026-01-30"] }
]
}
Transfer. An impeded feast of sufficient rank moves to the next free day — again if
that is occupied (a chain: end-of-Advent, Christmas octave, Holy Week); if none before a
cutoff, not observed this year. So a feast can arrive from an earlier date
(transferred_from/transferred_to), shown on the new date's saints entries via from
(§6). outcome distinguishes transferred (moved away) / not-observed / omitted
(stayed, nothing said).
Note — resumed Sundays are a different motion. (Open.) An impeded Sunday after Epiphany/Pentecost can be resumed weeks later (before Advent) — transfer-like but not feast-shaped, so the chain doesn't fit. Unmodelled: it shows only on its celebrated date, no link back. Open: a
resumes:relation.
5. Seasons and colour
"season": { "id": "lent", "arc": "after-epiphany", "week": 3 }
// arc: 1962 counting arc, null for OF; week: within the arc (see note)
Ids: advent · christmas · (1962:) epiphany · septuagesima · passiontide ·
pentecost (arc after-pentecost) · (both:) lent · easter · (OF:) ordinary.
Ascensiontide is within easter; the 1962 time after Pentecost is pentecost +
arc, not a separate id. Weeks are 1-based and Sunday-anchored (the week of a
mid-week arc start — Ash Wednesday's days, the days after Epiphany — is week 1).
The OF Christmas season runs through the eve of the Baptism; 1962 Christmastide
yields to Epiphanytide at 6 Jan. Seasons drive colour, the Mass chant swaps (§7),
whether Gloria/Alleluia/Te Deum are said, the ferial/Common fallback.
Note — what
weekcounts. Not a global counter. 1962 restarts per arc —after-epiphanyandafter-pentecosteach start at 1 (soarcdisambiguates); the after-Pentecost count varies 23–28 yearly, the last after-Epiphany Sundays resumed before Advent (§4). OF Ordinary Time is one 1–34 numbering that jumps across Lent–Easter (…8, [Lent/Easter], 11…);arcnull.
Note —
seasonis the celebrated position, scalar by choice. A day can sit in two frames (a Christmas-octave day ischristmas+ octave member; a resumed Sunday is in its pre-Advent slot while originating after Epiphany). One object (celebrated position), octave separate (§9); a resumed Sunday's origin frame is lost (open, §4). Rare enough.
Note — colour is
{ default, alternatives[] }, never scalar. Gaudete/Laetare add rose; a Requiem is black-or-violet; some vigils/votives differ. Values: white, red, green, violet, rose, black, gold.
6. Commemorations and the saints block
A lower, impeded office kept as a fragment of prayers in the winner's office — never "a second Mass".
- Composition — Mass: a 2nd/3rd Collect, Secret, Postcommunion. Lauds/Vespers: antiphon + versicle + collect. (Matins: sometimes a ninth lesson.)
- Two grades —
privileged(Sundays, I-class feasts, Advent/Lent ferias, Ember days, Christmas-octave days, Major Litanies — Lauds, Vespers, all Masses) vsordinary(Lauds/Vespers + low Masses only; omitted at Solemn Mass). - Order + cap — ordered (privileged first; higher class first; fixed within-class), truncated by rule at the oration cap (the truncation is itself information).
- Days that admit none — Palm Sunday, the Vigil of Pentecost, the highest days: empty by rule (with reason) ≠ "none fell today" (absent).
"commemorations": [
{ "name": { "la": "S. Agnetis … secundo", "en": "St Agnes, 2nd feast" },
"rank": { "label": "commemoration", "precedence": 110 },
"kind": "ordinary", "prayers": { "collect": { … } }, "id": "agnes-secundo" }
]
Note — cap is computed; imperated collects are separate. The cap is per-day computed (season-dependent), not a constant 3. An imperated (commanded) collect (added by bishop/ rubrics) is not a saint-commemoration — own field
imperated_collects[]on the Mass.
Note — Last Gospel in scope, Preface not. When a feast displaces a Sunday (1962), the commemorated Sunday's Gospel becomes the proper Last Gospel — calendar-selected, in scope (
last_gospel). The day's Preface is rank/season-driven but belongs to the Ordinary — a Boundary (a label at most, not text).
saints — "whose feast is it?" A commemoration is the loser of a precedence
contest, so the headline saint is never in saints.commemorated; and a loser is not
always commemorated:
| outcome | their feast on this date this year? | where |
|---|---|---|
celebrated | yes — they won | saints.celebrated (and celebration, the singular headline) |
commemorated | yes — observed minimally | saints.commemorated |
omitted | yes — date is theirs, nothing said (admit-none days / cap exceeded) | resolution now; saints.omitted when transfer chains land (additive) |
arrived | yes — transferred in from an earlier date | saints.celebrated/commemorated, gaining a from date (additive) |
transferred (away) | no — moved off this date | origin's resolution, not here |
The block is keyed by outcome rather than tagging a flat list, so the common question
("who is celebrated today?") is one key access, and new outcomes extend the shape
without breaking it. The Martyrology's remembered sits alongside as the third tier:
not a resolution outcome at all, but the Church's full remembrance of the date.
"saints": {
"celebrated": [ { "name": {…}, "id": "lawrence", "person": "lawrence", "rank": {…} } ],
"commemorated": [ { "name": {…}, "id": "agnes-secundo", "rank": {…} } ],
"remembered": [ { "name": "St Tychicus" } ] // + "note" with ?include=martyrology
}
7. Mass
The propers (vs the fixed Ordinary), ordered: Introit · Collect · Epistle · Gradual/Tract/Alleluia · [Sequence] · Gospel · Offertory · Secret · Communion · Postcommunion. Several are scripture readings (OF: first/second + psalm; traditional: epistle/gospel); the chant texts (Introit, Gradual, Offertory, Communion) are proper antiphons. Each slot is tagged by role, so one shape serves every rite:
{ "role": "gospel", "ref": "Luc 14:16-24",
"text": { "la": "In illo témpore…", "en": "…" },
"edition": { "la": "vulgate", "en": "douay" } } // which edition produced each text (ids per /liturgy/v1/translations)
// ref = uncopyrightable citation, present when known, independent of text + translation
Note — translation is a per-text axis, orthogonal to language (§10). Only scripture
textcarries it; theref, names, rubrics, and chant antiphons do not.editionreports the translation actually used per language (after thetranslationsrequest + per-language default fallback), so a client that asked for one and got another (unavailable for that passage) can tell. All editions we serve are public-domain and held internally.
Note —
readings[]is variable-length + ordered, not a fixed Epistle/Gospel template: an Ember Saturday has up to six lessons, the Easter Vigil a series of prophecies (12 older / 4 in 1962); multiplerole: "lectio"entries distinguished by position.
Note — interlection chant + Sequence. The between-readings slot is one role (
interlection) varying by season — Gradual / Tract (penitential) / double Alleluia (Eastertide). The Sequence is a separate slot following the Alleluia:role: "sequence",obligation: obligatory | optional(Easter/Pentecost obligatory; Lauda Sion/ Stabat Mater optional; Dies Irae Requiem-bound).
One date, several Masses — masses is a list (singular = a list of one):
- prescribed multiplicity — Christmas's three (Midnight/Dawn/Day), the great vigils — all said.
- optional choice — OF optional memorial vs feria — one is said.
- prescribed Requiem — when the day is a Requiem (All Souls = three) — in scope; discretionary votive / ad-hoc Requiem are out (Boundaries).
"masses": [
{ "id": "midnight", "name": {…}, "option_type": "prescribed", "colour": {…}, "readings": [ … ] },
{ "id": "dawn", "option_type": "prescribed", … }, { "id": "day", "option_type": "prescribed", … }
]
// option_type: prescribed (all said) | optional (a choice)
8. Office (Divine Office / Liturgy of the Hours)
Eight hours: matins · lauds · prime · terce · sext · none · vespers ·
compline (Prime suppressed in OF; a calendar advertises its hours). Each: psalms/
canticles framed by antiphons; hymn; chapter + versicle + responsory;
collect. Proper gospel canticles by hour — Benedictus (Lauds), Magnificat (Vespers), Nunc
Dimittis (Compline). Matins is larger — one/three nocturns of psalms + lessons, Te
Deum on festive days.
{
"hour": "vespers",
"psalmody": [ { "antiphon": {…}, "psalm": { "ref": "Ps 109", "text": {…} } } ],
"chapter": { "ref": "…", "text": {…} }, "hymn": {…}, "versicle": {…},
"canticle": { "antiphon": {…}, "text": { "la": "Magnificat…", "en": "…" } },
"collect": {…}, "commemorations": [ /* §6 — Lauds & Vespers only */ ]
}
Assembly by layers — Proper → applicable Common(s) → Psalter, each element falling through independently (collect from the Proper, psalms from the Psalter, etc.).
Note — the Common layer is composite.
titlesis a set (["martyr","bishop"]→ both Commons compose) and each Common holds several formularies by season — "resolve across the applicable Commons, pick the seasonally-correct formulary", not "one title → one Common".
First vs second Vespers (concurrence) — a feast has a first Vespers the eve before and a second Vespers the day itself; where they meet, precedence (§4) decides which is sung and whether the other is commemorated. "Vespers on D" is ambiguous — the two are distinct objects.
Note — split-source Vespers is a Boundary. Concurrence isn't always a clean swap: psalms from one office, chapter/hymn/collect a capitulo from another — one Vespers from two offices, which Proper→Common→Psalter can't express. We serve the resolved Vespers + outcome, not the element split.
9. Octaves, vigils, privileged-season days
Octaves — a first-rank feast prolonged eight days; each infra octavam day and the dies
octava is its own ranked, impedable day. 1960 keeps three (Christmas, Easter, Pentecost).
octave: { of, day } — day is a position; the actual class is in rank (Christmas-octave
days are not uniform: 26–28 Dec III-class, Octave Day II-class).
Note — octave type out of scope at 1960+. Pre-1955 classified octaves (privileged/ common/simple); 1960 flattened to three, so no
typetoday. A pre-1955 edition (§2) would needoctave_type.
Vigils — the liturgical eve of a great feast, a separate day on the prior date with its own Mass/office; can fall within another octave.
Note — vigils are typed.
vigil_type: ordinary(commemorated when impeded) |privileged(Christmas Eve, outranks a Sunday) |paschal(Easter Vigil — start of the Triduum). Rank is a property of the day (rank).
Privileged-season temporale days — day-types with their own proper Masses, ranked and
commemoration-privileged (rank.cycle: "temporale"), not plain ferias: Advent ferias from
17 Dec (the "O" days); Lenten weekday propers; Ember Days (Quattuor Tempora);
Rogation Days (Major Litany 25 Apr + Minor Litanies) — procession: true.
Note — the processional litany is unserved content. The Litany of the Saints in procession is neither Mass proper nor Office hour — a fourth content type with no home in §7/§8. Day/Mass/rank are in scope;
procession: trueflags it; the litany text is out.
10. Multilingual text
Every human-readable string is a language map { "la": …, "en": …, "fr": … }:
- Latin is the floor — always present, public-domain, never filtered out.
- Languages are data, not schema — adding
fr/esis another key; a request may narrow languages (Latin always kept). - Citation ≠ text — a
refis a fact that may stand alone;textis its translatable, possibly-absent rendering.
Language vs translation — two axes. Language (Accept-Language) picks the key in the
map; translation (translations query param) picks, for scripture text only, which
public-domain edition fills that key. They compose: Accept-Language: en + translations=cpdv
→ the English text rendered from the CPDV. Editions are data too (per language: en →
douay | cpdv; la → vulgate), each with a per-language default;
the resolved choice is reported on every reading via edition (§7) and echoed at the response
level as editions. Non-scripture strings (names, rubrics, chant antiphons) have a
single canonical rendering per language and ignore the axis. All editions are held internally
(public-domain corpora vendored in the service); none is fetched from a third party.
11. Saints
Two levels. A rite-scoped entry (§3) is that rite's celebration — its own date, rank, name. Above it sits a canonical person that indexes the same saint across rites, so you can see all calendars at once — including when the rites keep different dates (Catherine of Siena: OF 29 Apr, 1962 30 Apr; Ephrem: OF 9 Jun, 1962 18 Jun). The person is an index, not a flattened truth: each rite's data stays its own. Unification is name normalisation (honorifics/title clauses stripped, orthographic variants folded) plus a name-variant merge guarded by feast-date proximity — two different saints sharing a name (the many Felixes) can never false-merge — plus a small editorial alias list for reform-shifted names the guard rightly refuses (Bridget of Sweden's Oct→Jul move).
Person — all calendars at once (GET /liturgy/v1/saints/john-of-the-cross):
{
"person": "john-of-the-cross", "name": { "la": "S. Ioannes a Cruce", "en": "St John of the Cross" },
"calendars": {
"traditional": { "id": "ioannes-a-cruce", "date": "11-24", "rank": "class-3",
"titles": ["confessor","doctor"], "ref": "/liturgy/v1/calendars/traditional/saints/ioannes-a-cruce" },
"ordinary": { "id": "john-of-the-cross", "date": "12-14", "rank": "memorial",
"titles": ["doctor"], "ref": "/liturgy/v1/calendars/ordinary/saints/john-of-the-cross" }
}
}
Each rite block carries both its local id and a ref (the down-link); the rite-scoped entry
carries person (the up-link, §3). The two slug namespaces never strand a client.
Search disambiguates the several St Johns — name alone collides, so each carries titles
- feast date(s). Global search shows which calendars keep each; rite-scoped search lists
only that sanctorale. Results are the collection envelope (Endpoints),
Accept-Languagefor text:
// GET /liturgy/v1/saints?q=john (Accept-Language: en)
{ "results": [
{ "person": "john-the-baptist", "name": {…}, "titles": ["precursor"], "calendars": ["traditional","ordinary"],
"feasts": [ { "date": "06-24", "kind": "nativity" }, { "date": "08-29", "kind": "beheading" } ] },
{ "person": "john-of-the-cross", "name": {…}, "titles": ["doctor"], "calendars": ["traditional","ordinary"] }
],
"query": { "q": "john" }, "page": { "total": 2, "next": null } }
- One result per person,
feasts[]listing each occurrence (a saint can have several — Nativity + Beheading; an octave day; a secundo feast §6).dateisMM-DD(fixed) or a moveableanchor(§2). - Match is across language name-keys, diacritic-insensitive, Latin floor.
qmatches the name;title/monthare separate filters. - Rite-scoped search (
/calendars/{cal}/saints) returns local slugs and that rite's date/rank/titles only; a saint absent from a rite is absent from its results.
Boundaries (not served)
- Chant & ceremonial, and the fixed Ordinary of the Mass — we serve the proper + the calendar that selects it.
- First Vespers (
…/office/first-vespers) — owned by concurrence resolution, which isn't computed yet; the path 404s rather than serving second-Vespers content under the wrong name.vespers/second-vespers(the day's own) are served. - Saint names are English-only for now — the bundled sanctorale carries no Latin names, so
saint
nameblocks are{ en }(the one exception to the Latin floor, which holds for all liturgical text); Latin fills in as rite data is indexed. - The Preface (Ordinary) — though a commemorated Sunday's Last Gospel (proper) is served (§6).
- Processional litany text (Rogation/Major-Litany days, §9) — the day is in scope.
- Split-source concurred Vespers (§8) — the resolved Vespers is served, not the split.
- Discretionary votive / ad-hoc funeral/nuptial Masses (prescribed Requiem days are in, §7).
- Local/proper calendars beyond the three rites, the per-conference transfer/precept tables, and region-proper feasts (patrons, which do affect precedence) — data over time (§2).
- Arbitrary historical editions — only the three merged editions; no year-of-rubrics
selector (an
editionaxis is future, §2). - Two simultaneous season frames (a resumed Sunday's origin, §5).
- Eastern (Julian) computus — Gregorian Easter is assumed (§2).
- Vernacular titles of temporal days — Latin is the always-present floor; the English name is a best-effort overlay limited by the source data (Divinum Officium keeps temporal/ feast-of-the-Lord titles in Latin), so ~⅓ of 1962/Dominican days carry no English name (the readings/Office text are still translated where a free version exists). Sanctoral feasts get their English name where the source has one.
- Transfer/omission computation (§4/§6) — the engine resolves occurrence (winner +
commemorations) but not yet the transfer machinery, so
saintscurrently carries onlycelebrated/commemorated/rememberedandresolution.displacedonlycommemorated; theomitted/transferred/not-observedoutcomes andchainare in the shape and fill in when transfer resolution lands (the DOTabulae/Transfertables are vendored for it). We emit absence rather than a guessed transfer.
Open questions
- Liturgical-day vs civil-date key (§3) — the spillover relations' shape.
- Conditional precedence (§4) — expose the rule, or only its outcome?
- Resumed Sundays (§4) — a
resumes:relation distinct from transfer.
Sources
Ranking of liturgical days · Commemoration · Occurrence · Proper · Roman Breviary · General Roman Calendar 1960 · 1960 General Rubrics · romcal (calendar-model prior art).
Data provenance (all held internally — no runtime third-party calls). Calendar + propers +
Office: the public-domain Divinum Officium corpus, vendored. Martyrology: the
public-domain 1916 Baltimore edition, vendored (data/martyrology/). Scripture text: public-domain
editions, vendored — la: Clementine Vulgate (· Nova Vulgata where free); en: Douay-Rheims
(Challoner), Catholic Public Domain Version. Citations are uncopyrightable facts. Nothing is
fetched from an external service at request time.