Platform Services
Salat API Service
A read only REST API for Moroccan prayer times. It serves two independent sources the official Habous timetable, collected and archived daily, and the sec approach v2 astronomical calculation plus a unified endpoint that prefers official data and transparently falls back to calculation.
Reference
Overview
Every endpoint is a GET. Responses are JSON. There are no write, sync, or
delete endpoints collection runs on a schedule or from the command line, never over HTTP. All times are in
Africa/Casablanca and formatted HH:mm (or HH:mm:ss at second
precision).
Reference
Base URL and access
The port is set by SERVER_PORT (default 8081). The API is
deliberately unauthenticated: it exposes public prayer time data and holds no user state.
Data model
City coverage model
Four nested groups. Confusing them is the most common integration mistake, so the API keeps them distinct collecting official data for a city never makes it calculable.
| Group | Count | What it means | Identifier |
|---|---|---|---|
| Official villes | 191 | Cities in the official Habous directory eligible for collection. | habousCityId |
| Registry cities | 67 | Application cities with a slug, coordinates and aliases. | applicationCityId |
| Verified mappings | 60 | Registry cities linked to a confirmed official ville. | both |
| Sec approach | 48 | Cities with released offsets the only ones the calculation serves. | applicationCityId |
48 ⊆ 60 ⊆ 67, and the 60 ville targets ⊆ the 191.
Asking sec approach for one of the other 19 registry cities returns
422 SEC_APPROACH_CITY_UNSUPPORTED never a guess.
Endpoints
Service information
Snapshot of coverage counts, the active parameter version, and the freshness of collected data.
{
"applicationVersion": "0.1.0",
"officialCatalogueCount": 191,
"registryCityCount": 67,
"verifiedMappingCount": 60,
"secApproachSupportCount": 48,
"parameterVersion": "morocco-habous-secaproach-v2",
"latestSuccessfulSync": "2026-07-16",
"latestOfficialDate": "2026-08-14",
"latestArchiveStatus": "DEGRADED"
}Operational status: where data lives, whether the scheduler is on, per month archive
state, and the latest verification metrics. Check dataDirectory and
monthlyArchiveStatuses first when an official request unexpectedly returns 404.
{
"dataDirectory": "/srv/salat/data",
"schedulerEnabled": true,
"syncInProgress": false,
"officialCatalogueCount": 191,
"latestCatalogueRefresh": "2026-07-16T03:15:04Z",
"latestSuccessfulSync": "2026-07-16",
"latestFailedSync": null,
"monthlyArchiveStatuses": { "2026-07": "DEGRADED", "2026-08": "DEGRADED" },
"officialDataFreshnessDays": 0,
"secApproachAvailable": true,
"secApproachCityCount": 48,
"latestVerification": {
"date": "2026-08-14", "comparedCityCount": 48,
"exactCityCount": 46, "exactRatio": 0.958, "withinOneRatio": 1.0
}
}Endpoints
Official Habous data
Published times exactly as the Ministry of Habous releases them. These endpoints never calculate a fallback a missing day is a 404, not a guess.
List the official city catalogue with support flags. Filters combine with AND.
| Name | Type | Description |
|---|---|---|
| mapped | boolean optional | Only cities that have (or lack) a verified application mapping. |
| secApproachSupported | boolean optional | Only the 48 calculable cities when true. |
| available | boolean optional | Only cities with collected data in the latest archive. |
[
{
"habousCityId": "1",
"officialNameAr": "الرباط",
"applicationCityId": "rabat",
"mapped": true,
"secApproachSupported": true,
"available": true
}
]?secApproachSupported=true → 48 ·
?mapped=true → 60 · no filter → 191.
One official city by its ville id. 404
HABOUS_CITY_NOT_FOUND if the id is not in the catalogue.
GET /api/v1/habous/cities/by-application-id/{applicationCityId}Resolves the slug to its ville via the verified mapping:
404 APPLICATION_CITY_NOT_MAPPED if it has none.
Summaries of every stored Gregorian monthly archive.
[
{
"period": "2026-07",
"status": "DEGRADED",
"officialCatalogCityCount": 191,
"archiveCityCount": 191,
"completeCityCount": 0,
"partialCityCount": 191,
"unavailableCityCount": 0,
"expectedDaysPerCompleteCity": 31,
"lastSuccessfulSyncAt": "2026-07-16T03:15:20Z"
}
]| Status | Meaning |
|---|---|
| EMPTY | No validated rows yet. |
| PARTIAL | Rows exist: the month is still accruing (normal). |
| COMPLETE | Every enabled city has every day of the month. |
| DEGRADED | Has data, but a city failed permanently or the catalogue changed mid collection. |
The full monthly archive: every official city and its collected days.
404 OFFICIAL_MONTH_NOT_AVAILABLE if the month was never
assembled.
| Name | Type | Description |
|---|---|---|
| includeUnavailable | boolean default true | Include cities with no collected days (status UNAVAILABLE). |
{
"schemaVersion": "1.0",
"source": "HABOUS_OFFICIAL",
"period": "2026-07",
"periodType": "GREGORIAN_MONTH",
"timezone": "Africa/Casablanca",
"status": "DEGRADED",
"officialCatalogCityCount": 191,
"cityCount": 191,
"completeCityCount": 0, "partialCityCount": 191, "failedCityCount": 0,
"cities": [
{
"habousCityId": "1", "officialNameAr": "الرباط",
"applicationCityId": "rabat", "secApproachSupported": true,
"status": "PARTIAL", "daysExpected": 31, "daysAvailable": 16,
"days": [
{ "date": "2026-07-16", "fajr": "04:40", "sunrise": "06:25",
"dhuhr": "13:39", "asr": "17:19", "maghrib": "20:43", "isha": "22:13" }
]
}
]
}The same archive as a downloadable file.
One city's schedule within one month, by ville id.
GET /api/v1/habous/months/{year}/{month}/application-cities/{applicationCityId}
One official day. 404
OFFICIAL_DATA_NOT_AVAILABLE if that date was not collected.
| Name | Type | Description |
|---|---|---|
| date | date required | ISO YYYY-MM-DD. |
GET /api/v1/habous/application-cities/{applicationCityId}/prayer-times?date=YYYY-MM-DD{
"habousCityId": "1",
"officialNameAr": "الرباط",
"applicationCityId": "rabat",
"source": "HABOUS_OFFICIAL",
"day": {
"date": "2026-07-16", "fajr": "04:40", "sunrise": "06:25",
"dhuhr": "13:39", "asr": "17:19", "maghrib": "20:43", "isha": "22:13",
"sourceChecksum": "…", "firstObservedAt": "…", "lastObservedAt": "…"
}
}Endpoints
Sec approach calculation
The released v2 astronomical method: a reconstructed raw time plus a per city, per prayer constant second offset, floored to the minute. Available for any date, for the 48 supported cities.
Method metadata, including the honest validation window and accuracy disclaimer.
{
"parameterVersion": "morocco-habous-secaproach-v2",
"supportedCityCount": 48,
"formula": "publishedMinute = floor((rawSeconds + offsetSeconds[city][prayer]) / 60)",
"timezone": "Africa/Casablanca",
"precisionModes": ["minute", "second"],
"validatedWindow": "2026-06-17 through 2026-07-16",
"accuracyDisclaimer": "Validated on the 2026-06-17 through 2026-07-16 summer window. Not an annual accuracy claim."
}Exactly the 48 cities with released offsets the complete set the calculation serves.
[
{ "applicationCityId": "agadir", "displayName": "Agadir", "habousCityId": "117" },
{ "applicationCityId": "casablanca", "displayName": "Casablanca", "habousCityId": "58" }
]Calculated times for one date. 422
SEC_APPROACH_CITY_UNSUPPORTED for any city outside the 48.
| Name | Type | Description |
|---|---|---|
| date | date required | ISO YYYY-MM-DD. |
| precision | minute · second default minute | minute → HH:mm: second → HH:mm:ss. |
# curl no auth header curl "http://localhost:8081/api/v1/sec-approach/cities/rabat/prayer-times?date=2026-07-15"
{
"date": "2026-07-15",
"precision": "minute",
"times": {
"fajr": "04:39", "sunrise": "06:24", "dhuhr": "13:38",
"asr": "17:19", "maghrib": "20:43", "isha": "22:14"
}
}An inclusive date range. Capped at 366 days
(SALAT_MAXIMUM_RANGE_DAYS): a larger span returns
400 DATE_RANGE_TOO_LARGE.
| Name | Type | Description |
|---|---|---|
| from | date required | Range start, inclusive. |
| to | date required | Range end, inclusive. |
| precision | minute · second default minute | As above. |
[ { "date": "2026-07-01", "precision": "minute", "times": { /* … */ } }, /* one per day */ ]A whole calendar month of calculated times.
| Name | Type | Description |
|---|---|---|
| year | integer required | e.g. 2026. |
| month | integer required | 1–12: otherwise 400 INVALID_MONTH. |
| precision | minute · second default minute | As above. |
Endpoints
Unified best source
One endpoint that resolves official versus calculated and always tells you which answered. Fallback is never hidden.
| Name | Type | Description |
|---|---|---|
| date | date required | ISO YYYY-MM-DD. |
| source | best · official · sec-approach default best | best = validated official, else released sec approach. |
| precision | minute · second default minute | As above. |
{
"requestedSource": "BEST",
"resolvedSource": "HABOUS_OFFICIAL",
"fallbackUsed": false,
"times": { /* … */ }
}{
"requestedSource": "BEST",
"resolvedSource": "SEC_APPROACH",
"fallbackUsed": true,
"fallbackReason": "Official data is unavailable for the requested date",
"times": { /* … */ }
}source=official returns
404 when no official data exists (no silent fallback):
source=sec-approach forces the calculation.
Endpoints
Verification
Daily comparison of sec approach against official data, over the intersection of cities
that have both. Metrics use secApproachMinute − officialMinute.
The most recent daily report. 404 if none exists yet.
{
"date": "2026-08-14",
"parameterVersion": "morocco-habous-secaproach-v2",
"accuracyDisclaimer": "Validated on the 2026-06-17 … Not an annual accuracy claim.",
"comparedCityCount": 48, "exactCityCount": 46,
"overall": { "exactCount": 280, "exactRatio": 0.972,
"withinOneCount": 288, "withinOneRatio": 1.0,
"meanAbsoluteDifference": 0.03, "maxAbsoluteDifference": 1 },
"metricsByCity": { /* per applicationCityId */ },
"metricsByPrayer": { /* per prayer */ },
"officialChecksums": [ "…" ],
"cities": [ /* per city, per prayer diffs */ ]
}The report for a specific date.
One city's per prayer comparison for a date.
Every daily report whose date falls in the month.
Reference
Errors
Errors use RFC 7807 application/problem+json with a stable
code. Server side causes are logged, never returned no stack traces, paths, or internals.
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "No sec-approach offsets for city 'berkane'. Only the 48 trained cities are supported…",
"instance": "/api/v1/sec-approach/cities/berkane/prayer-times",
"code": "SEC_APPROACH_CITY_UNSUPPORTED",
"correlationId": "f42ab51a-…",
"timestamp": "2026-07-16T10:47:51Z"
}| Status | Code | When |
|---|---|---|
| 400 | INVALID_DATE | Malformed date, or to before from. |
| 400 | INVALID_MONTH | Month outside 1–12, or bad year. |
| 400 | INVALID_PRECISION | precision not minute/second. |
| 400 | INVALID_SOURCE | source not best/official/sec-approach. |
| 400 | DATE_RANGE_TOO_LARGE | Range exceeds 366 days. |
| 404 | HABOUS_CITY_NOT_FOUND | Unknown official ville id. |
| 404 | APPLICATION_CITY_NOT_MAPPED | Slug has no verified mapping. |
| 404 | OFFICIAL_DATA_NOT_AVAILABLE | No official row for that city/date. |
| 404 | OFFICIAL_MONTH_NOT_AVAILABLE | No archive for that month. |
| 422 | SEC_APPROACH_CITY_UNSUPPORTED | City exists but has no offsets (one of the 19). |
| 500 | ARCHIVE_CORRUPTED | A stored archive could not be read. |
| 500 | CALCULATION_FAILED | An unexpected error. |
Reference
Concepts and operations
The official source publishes one Hijri month at a time, which straddles two
Gregorian months (e.g. 17 Jun – 16 Jul). Each day is filed by its own Gregorian date, so one source page feeds
two monthly archives. A Gregorian month only becomes COMPLETE once both overlapping Hijri
pages have been collected expect PARTIAL in between.
Collection runs on a schedule (default 03:15 Africa/Casablanca) or from the command line. There is no endpoint to trigger it.
# collect now (all official cities), into an explicit data directory java -jar salat-api-service.jar --salat.job=sync-now \ --spring.main.web-application-type=none --salat.data-directory=./data # reassemble one month from stored snapshots no network java -jar salat-api-service.jar --salat.job=rebuild-month \ --salat.job.period=2026-07 --spring.main.web-application-type=none # disable the scheduler during development HABOUS_SYNC_ENABLED=false java -jar salat-api-service.jar
| Environment variable | Default | Purpose |
|---|---|---|
| SERVER_PORT | 8081 | HTTP port. |
| SALAT_DATA_DIRECTORY | ./data | Where archives and reports are read/written. |
| HABOUS_SYNC_ENABLED | true | Enable the daily collection scheduler. |
| HABOUS_SYNC_CRON | 0 15 3 * * * | Collection schedule (Africa/Casablanca). |
| SALAT_MAXIMUM_RANGE_DAYS | 366 | Cap on the sec approach range endpoint. |