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).

Two sources, never merged
Official endpoints return published Habous times exactly. Sec approach endpoints return the released calculation. The unified endpoint labels which one answered fallback is always visible.
Forward only archive
The official source publishes only the current Hijri month, which spans two Gregorian months. History accrues going forward and is never back filled.

Reference

Base URL and access

Base URL
http://localhost:8081/api/v1
Auth
None no Authorization header required
Health
GET /actuator/health  →  {"status":"UP"}
Tracing
Every response returns an X-Correlation-Id header

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.

GroupCountWhat it meansIdentifier
Official villes191Cities in the official Habous directory eligible for collection.habousCityId
Registry cities67Application cities with a slug, coordinates and aliases.applicationCityId
Verified mappings60Registry cities linked to a confirmed official ville.both
Sec approach48Cities 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

GET/api/v1/info

Snapshot of coverage counts, the active parameter version, and the freshness of collected data.

Response 200
{
  "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"
}
GET/api/v1/data-status

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.

Response 200
{
  "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.

GET/api/v1/habous/cities

List the official city catalogue with support flags. Filters combine with AND.

Query parameters
NameTypeDescription
mappedboolean optionalOnly cities that have (or lack) a verified application mapping.
secApproachSupportedboolean optionalOnly the 48 calculable cities when true.
availableboolean optionalOnly cities with collected data in the latest archive.
Response 200 array
[
  {
    "habousCityId": "1",
    "officialNameAr": "الرباط",
    "applicationCityId": "rabat",
    "mapped": true,
    "secApproachSupported": true,
    "available": true
  }
]

?secApproachSupported=true → 48 · ?mapped=true → 60 · no filter → 191.

GET/api/v1/habous/cities/{habousCityId}

One official city by its ville id. 404 HABOUS_CITY_NOT_FOUND if the id is not in the catalogue.

Also by application slug
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.

GET/api/v1/habous/months

Summaries of every stored Gregorian monthly archive.

Response 200 array
[
  {
    "period": "2026-07",
    "status": "DEGRADED",
    "officialCatalogCityCount": 191,
    "archiveCityCount": 191,
    "completeCityCount": 0,
    "partialCityCount": 191,
    "unavailableCityCount": 0,
    "expectedDaysPerCompleteCity": 31,
    "lastSuccessfulSyncAt": "2026-07-16T03:15:20Z"
  }
]
Archive status values
StatusMeaning
EMPTYNo validated rows yet.
PARTIALRows exist: the month is still accruing (normal).
COMPLETEEvery enabled city has every day of the month.
DEGRADEDHas data, but a city failed permanently or the catalogue changed mid collection.
GET/api/v1/habous/months/{year}/{month}

The full monthly archive: every official city and its collected days. 404 OFFICIAL_MONTH_NOT_AVAILABLE if the month was never assembled.

Query parameters
NameTypeDescription
includeUnavailableboolean default trueInclude cities with no collected days (status UNAVAILABLE).
Response 200 envelope (cities truncated)
{
  "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" }
      ]
    }
  ]
}
GET/api/v1/habous/months/{year}/{month}/file

The same archive as a downloadable file.

Content-Type
application/json
Content-Disposition
attachment; filename="habous-prayer-times-2026-07.json"
GET/api/v1/habous/months/{year}/{month}/cities/{habousCityId}

One city's schedule within one month, by ville id.

Also by application slug
GET /api/v1/habous/months/{year}/{month}/application-cities/{applicationCityId}
GET/api/v1/habous/cities/{habousCityId}/prayer-times

One official day. 404 OFFICIAL_DATA_NOT_AVAILABLE if that date was not collected.

Query parameters
NameTypeDescription
datedate requiredISO YYYY-MM-DD.
Also by application slug
GET /api/v1/habous/application-cities/{applicationCityId}/prayer-times?date=YYYY-MM-DD
Response 200
{
  "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.

GET/api/v1/sec-approach/info

Method metadata, including the honest validation window and accuracy disclaimer.

Response 200
{
  "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."
}
GET/api/v1/sec-approach/cities

Exactly the 48 cities with released offsets the complete set the calculation serves.

Response 200 array
[
  { "applicationCityId": "agadir", "displayName": "Agadir", "habousCityId": "117" },
  { "applicationCityId": "casablanca", "displayName": "Casablanca", "habousCityId": "58" }
]
GET/api/v1/sec-approach/cities/{applicationCityId}/prayer-times

Calculated times for one date. 422 SEC_APPROACH_CITY_UNSUPPORTED for any city outside the 48.

Query parameters
NameTypeDescription
datedate requiredISO YYYY-MM-DD.
precisionminute · second default minuteminuteHH:mm: secondHH:mm:ss.
Example
# curl no auth header
curl "http://localhost:8081/api/v1/sec-approach/cities/rabat/prayer-times?date=2026-07-15"
Response 200
{
  "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"
  }
}
GET/api/v1/sec-approach/cities/{applicationCityId}/prayer-times/range

An inclusive date range. Capped at 366 days (SALAT_MAXIMUM_RANGE_DAYS): a larger span returns 400 DATE_RANGE_TOO_LARGE.

Query parameters
NameTypeDescription
fromdate requiredRange start, inclusive.
todate requiredRange end, inclusive.
precisionminute · second default minuteAs above.
Response 200
[ { "date": "2026-07-01", "precision": "minute", "times": { /* … */ } }, /* one per day */ ]
GET/api/v1/sec-approach/cities/{applicationCityId}/prayer-times/month

A whole calendar month of calculated times.

Query parameters
NameTypeDescription
yearinteger requirede.g. 2026.
monthinteger required112: otherwise 400 INVALID_MONTH.
precisionminute · second default minuteAs above.

Endpoints

Unified best source

One endpoint that resolves official versus calculated and always tells you which answered. Fallback is never hidden.

GET/api/v1/prayer-times/cities/{applicationCityId}
Query parameters
NameTypeDescription
datedate requiredISO YYYY-MM-DD.
sourcebest · official · sec-approach default bestbest = validated official, else released sec approach.
precisionminute · second default minuteAs above.
Resolved to official
{
  "requestedSource": "BEST",
  "resolvedSource": "HABOUS_OFFICIAL",
  "fallbackUsed": false,
  "times": { /* … */ }
}
Fell back to calculation
{
  "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.

GET/api/v1/verification/latest

The most recent daily report. 404 if none exists yet.

Response 200 (abridged)
{
  "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 */ ]
}
GET/api/v1/verification/dates/{date}

The report for a specific date.

GET/api/v1/verification/dates/{date}/cities/{applicationCityId}

One city's per prayer comparison for a date.

GET/api/v1/verification/months/{year}/{month}

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"
}
StatusCodeWhen
400INVALID_DATEMalformed date, or to before from.
400INVALID_MONTHMonth outside 1–12, or bad year.
400INVALID_PRECISIONprecision not minute/second.
400INVALID_SOURCEsource not best/official/sec-approach.
400DATE_RANGE_TOO_LARGERange exceeds 366 days.
404HABOUS_CITY_NOT_FOUNDUnknown official ville id.
404APPLICATION_CITY_NOT_MAPPEDSlug has no verified mapping.
404OFFICIAL_DATA_NOT_AVAILABLENo official row for that city/date.
404OFFICIAL_MONTH_NOT_AVAILABLENo archive for that month.
422SEC_APPROACH_CITY_UNSUPPORTEDCity exists but has no offsets (one of the 19).
500ARCHIVE_CORRUPTEDA stored archive could not be read.
500CALCULATION_FAILEDAn unexpected error.

Reference

Concepts and operations

The Hijri to Gregorian split

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.

Populating data (no HTTP write path)

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
Key configuration
Environment variableDefaultPurpose
SERVER_PORT8081HTTP port.
SALAT_DATA_DIRECTORY./dataWhere archives and reports are read/written.
HABOUS_SYNC_ENABLEDtrueEnable the daily collection scheduler.
HABOUS_SYNC_CRON0 15 3 * * *Collection schedule (Africa/Casablanca).
SALAT_MAXIMUM_RANGE_DAYS366Cap on the sec approach range endpoint.