# SolatCal

> Turns official JAKIM e-Solat prayer times for Malaysia into an iCalendar
> (.ics) feed that any calendar app can subscribe to. 60 zones,
> no account, no key, free. Unofficial: not affiliated with JAKIM.

All times are Asia/Kuala_Lumpur (UTC+08:00, no DST) and come from JAKIM
e-Solat unchanged. SolatCal performs no astronomical calculation of its own.

`/api/*` and `/mcp` allow 300 requests a minute per
client IP. Static pages are exempt. Responses state the limit in
`RateLimit-Policy` and `X-RateLimit-Limit`. Exceeding it returns HTTP 429
with `Retry-After`.

## How the times are checked

SolatCal copies JAKIM's published times and calculates none of its own, so
there is no calculation method or adjustment that could differ from JAKIM.
An automated test suite covers every rule that turns a JAKIM day into
events. The published feeds are also checked end to end: a separate script
downloads JAKIM's year for every zone and the live feed for the same zone,
parses both independently, and compares every event with JAKIM's time for
that date and prayer. The latest check, on 2026-09-25 (UTC), compared all 34920 events in the feeds for all 60 zones, from 2026-09-26 to 2026-12-31, with JAKIM's published times: every event matched to the minute and none were missing.
See [About](https://solatcal.com/about/#how-the-times-are-checked).

## Finding the zone for a place

JAKIM publishes times per zone and names each zone by its districts, not its
towns: Subang Jaya, Petaling Jaya and Cyberjaya are all in `SGR01`, listed by
JAKIM as "Petaling" and "Sepang". To go from a place to its times:

- `GET https://solatcal.com/api/find?q=Subang+Jaya` returns the zone, with links to its
  page, today's times and its feed. It knows 524 towns and areas, short
  forms (KL, PJ, JB, KK), abbreviations (Sg, Bt, Kg) and longer text such as
  "Shah Alam, Selangor". `?lat=3.07&lon=101.59` finds the zone for a position.
- Over MCP, `get_prayer_times` and `get_calendar_feed` take `place` instead
  of `zone`, and `find_zone` answers the question on its own.
- Every zone page lists the towns it covers, and every state has a page with
  today's times for all its zones, for example
  [https://solatcal.com/prayer-times/selangor](https://solatcal.com/prayer-times/selangor).

## Pages

Every page below also answers `Accept: text/markdown` with a Markdown
version at the same address (`Content-Type: text/markdown`, `Vary: Accept`),
404s included.

- [English](https://solatcal.com/): the feed builder.
- [Bahasa Malaysia](https://solatcal.com/ms/): the same builder in Malay.
- [Prayer times by zone](https://solatcal.com/prayer-times/) and
  [Waktu solat mengikut zon](https://solatcal.com/ms/waktu-solat/): every zone, by state.
- One page per zone and language, for example
  [https://solatcal.com/prayer-times/sgr01](https://solatcal.com/prayer-times/sgr01) and
  [https://solatcal.com/ms/waktu-solat/sgr01](https://solatcal.com/ms/waktu-solat/sgr01):
  today's times, the month's timetable (`?month=YYYY-MM` for another month
  of the current year), every town the zone covers and a builder set to
  that zone.
- One page per state and language, for example
  [https://solatcal.com/prayer-times/johor](https://solatcal.com/prayer-times/johor) and
  [https://solatcal.com/ms/waktu-solat/johor](https://solatcal.com/ms/waktu-solat/johor):
  today's times for every zone in the state side by side, and each zone's towns.
- [About](https://solatcal.com/about/): where the times come from and how
  they are checked. [Privacy](https://solatcal.com/privacy/): what is
  processed and where. [Contact](https://solatcal.com/contact/): how to
  reach the maker and report a wrong time. All three also in Malay.
- [API docs](https://solatcal.com/docs): this document as a web page.

## Calendar feed

`GET https://solatcal.com/api/feed.ics?zone=<ZONE>&...`

Returns `text/calendar`. Subscribe to the URL for a feed that updates itself,
or add `&download=1` for a one-off file. Swap the scheme for `webcal:` to
make Apple Calendar and most desktop clients subscribe directly. Google
Calendar's subscribe deep link also needs the `webcal://` form.

| Parameter | Values | Default | Meaning |
|---|---|---|---|
| `zone` | one of the 60 zone codes below | **required** | Which JAKIM zone |
| `period` | `month`, `quarter`, `year` | `year` | Date range for a one-off download. `month` and `quarter` run forward from today; `year` runs to 31 December. Subscriptions always carry the rest of the year regardless |
| `prayers` | comma separated, any of `fajr`, `syuruk`, `dhuhr`, `asr`, `maghrib`, `isha` | `fajr,dhuhr,asr,maghrib,isha` | Which prayers become events |
| `align` | `exact`, `early5`, `early10`, `block15` | `exact` | Event start: at the solat time, 5 or 10 minutes before, or snapped down to the containing 15 minute block |
| `duration` | integer 5 to 120 | `15` | Event length in minutes. With `block15` the end also rounds up to the next quarter hour |
| `busy` | `busy`, `free` | `busy` | Sets `TRANSP` to `OPAQUE` or `TRANSPARENT` |
| `alarm` | `none`, `0`, `5`, `10` | `none` | Display reminder, that many minutes before the start |
| `ramadan` | `0`, `1` | `0` | With `1`, days JAKIM dates to Ramadan gain an Imsak event from imsak to Subuh, and the Maghrib event is titled as iftar |
| `hijri` | `none`, `major`, `daily` | `none` | All-day hijri date events, marked free: `major` for dates such as Awal Ramadan and Hari Raya Aidilfitri, `daily` for every day |
| `lang` | `en`, `ms` | `en` | Language of the labels inside the calendar. Prayer names are Malay either way |
| `download` | `1` | off | Adds a `Content-Disposition: attachment` header |

Any unrecognised value returns HTTP 400 with a JSON body of the form
`{"error": "...", "status": 400}` naming the offending parameter.

### Examples

Five daily prayers for Kuala Lumpur, on time, 15 minutes:

    https://solatcal.com/api/feed.ics?zone=WLY01

Selangor (Gombak and Petaling), snapped to 15 minute blocks, 30 minutes,
marked free, 10 minute reminder, Malay labels:

    https://solatcal.com/api/feed.ics?zone=SGR01&align=block15&duration=30&busy=free&alarm=10&lang=ms

Kuching with Ramadan events and the major hijri dates:

    https://solatcal.com/api/feed.ics?zone=SWK08&ramadan=1&hijri=major

Download the next three months for Johor Bahru including Syuruk:

    https://solatcal.com/api/feed.ics?zone=JHR02&period=quarter&prayers=fajr,syuruk,dhuhr,asr,maghrib,isha&download=1

Subscribe from Apple Calendar:

    webcal://solatcal.com/api/feed.ics?zone=WLY01

### Event shape

One `VEVENT` per prayer per day. `SUMMARY` uses the Malay names Subuh,
Syuruk, Zohor, Asar, Maghrib, Isyak; on Fridays Zohor is titled Jumaat.
`UID` is `<zone>-<YYYYMMDD>-<prayer>@<host>` and never changes, so
re-importing updates events instead of duplicating them. `DESCRIPTION`
always carries the exact JAKIM time, the zone, the hijri date and a source
link, even when `align` has moved the event start.

With `ramadan=1`, Ramadan days add an `Imsak` event (UID suffix `imsak`)
that starts at imsak, ends at Subuh and ignores `align`. The Maghrib event's
title becomes `Maghrib (Iftar)` or `Maghrib (Berbuka)`. With `hijri`,
all-day events (UID suffix `hijri`) carry the hijri date. On dates such as
Hari Raya the occasion name comes first, as in
`Hari Raya Aidilfitri · 1 Syawal 1448`. They are always
`TRANSPARENT` and never carry a reminder.

## Other JSON endpoints

The HTTP API is also described as OpenAPI 3.1 at `https://solatcal.com/openapi.json`,
and `GET https://solatcal.com/api` lists every endpoint with links to these docs.

- `GET https://solatcal.com/api/zones` - every zone code mapped to its districts.
- `GET https://solatcal.com/api/find?q=<PLACE>` or `?lat=<LAT>&lon=<LON>` - the zone for a
  town, district, state or position, best match first.
- `GET https://solatcal.com/api/places` - towns and districts per zone with coordinates,
  which the pages use to find a zone by name or by the visitor's location.
- `GET https://solatcal.com/api/preview?zone=<ZONE>` - today's times for one zone, already
  run through the same `align` and `duration` rules as the feed. Accepts the
  same parameters as the feed, including `ramadan` and `hijri` (all-day
  events come back under `allDay`). Also carries today's `imsak`, whether
  today is in Ramadan, and tomorrow's raw times under `tomorrow`, which is
  `null` on 31 December.
- `GET https://solatcal.com/api/health` - service and upstream status.

## MCP server

`POST https://solatcal.com/mcp` speaks Model Context Protocol over JSON-RPC 2.0. It is
stateless Streamable HTTP: no session id, no SSE, and responses are
`application/json`. `GET /mcp` returns 405. Bodies larger than 32 KB
return 413. Batched JSON-RPC arrays are refused. Browsers can call it from
any origin.

It serves protocol version `2026-07-28`, where every request carries its own
version, and the versions before it that open with `initialize`
(`2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`).

With `2026-07-28`, every request puts
`io.modelcontextprotocol/protocolVersion` and
`io.modelcontextprotocol/clientCapabilities` in `params._meta`, and sends the
`MCP-Protocol-Version` and `Mcp-Method` headers, plus `Mcp-Name` on
`tools/call`. Handled methods: `server/discover`, `tools/list`,
`tools/call`. A missing `_meta` field is HTTP 400 with JSON-RPC `-32602`. A
header that disagrees with the body is 400 with `-32020`. An unsupported
version is 400 with `-32022`, listing the supported versions in
`error.data.supported`. An unknown method is 404 with `-32601`.

With the earlier versions, handled methods are `initialize`,
`notifications/*` (202, no body), `ping`, `tools/list` and `tools/call`.
Anything else is JSON-RPC `-32601`.

In both, a `tools/call` for a tool that does not exist is `-32602`.

| Tool | Purpose |
|---|---|
| `find_zone` | The zone for a town, district, state, short form or position |
| `list_zones` | The 60 zones grouped by state, optionally filtered by state name or zone prefix |
| `get_prayer_times` | One day's times for a zone or a place, plus the hijri date |
| `get_calendar_feed` | A validated feed URL, webcal URL, download URL and a plain-language summary, for a zone or a place. Does not fetch times |

Bad tool input comes back as a normal result with `isError: true` and a
message naming the offending parameter, not as a protocol error.
`get_calendar_feed` validates through the same rules as the HTTP feed.

    curl -sX POST https://solatcal.com/mcp \
      -H 'content-type: application/json' \
      -H 'mcp-protocol-version: 2026-07-28' \
      -H 'mcp-method: tools/list' \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

## Zones

All 60 zones, with the places each is best known for. Each zone
page lists every town it covers.

| Zone | State | Best-known places | JAKIM districts |
|---|---|---|---|
| [JHR01](https://solatcal.com/prayer-times/jhr01) | Johor | Pulau Aur, Pulau Pemanggil | Pulau Aur dan Pulau Pemanggil |
| [JHR02](https://solatcal.com/prayer-times/jhr02) | Johor | Johor Bahru, Iskandar Puteri, Kulai | Johor Bahru, Kota Tinggi, Mersing, Kulai |
| [JHR03](https://solatcal.com/prayer-times/jhr03) | Johor | Kluang, Pontian | Kluang, Pontian |
| [JHR04](https://solatcal.com/prayer-times/jhr04) | Johor | Batu Pahat, Muar, Segamat | Batu Pahat, Muar, Segamat, Gemas Johor, Tangkak |
| [KDH01](https://solatcal.com/prayer-times/kdh01) | Kedah | Alor Setar, Jitra, Kubang Pasu | Kota Setar, Kubang Pasu, Pokok Sena (Daerah Kecil) |
| [KDH02](https://solatcal.com/prayer-times/kdh02) | Kedah | Sungai Petani, Kuala Muda, Yan | Kuala Muda, Yan, Pendang |
| [KDH03](https://solatcal.com/prayer-times/kdh03) | Kedah | Padang Terap, Sik | Padang Terap, Sik |
| [KDH04](https://solatcal.com/prayer-times/kdh04) | Kedah | Baling | Baling |
| [KDH05](https://solatcal.com/prayer-times/kdh05) | Kedah | Kulim, Bandar Baharu | Bandar Baharu, Kulim |
| [KDH06](https://solatcal.com/prayer-times/kdh06) | Kedah | Langkawi | Langkawi |
| [KDH07](https://solatcal.com/prayer-times/kdh07) | Kedah | Puncak Gunung Jerai | Puncak Gunung Jerai |
| [KTN01](https://solatcal.com/prayer-times/ktn01) | Kelantan | Kota Bharu, Pasir Mas, Tumpat | Bachok, Kota Bharu, Machang, Pasir Mas, Pasir Puteh, Tanah Merah, Tumpat, Kuala Krai, Mukim Chiku |
| [KTN02](https://solatcal.com/prayer-times/ktn02) | Kelantan | Gua Musang, Jeli, Lojing | Gua Musang (Daerah Galas Dan Bertam), Jeli, Jajahan Kecil Lojing |
| [MLK01](https://solatcal.com/prayer-times/mlk01) | Melaka | Melaka | SELURUH NEGERI MELAKA |
| [NGS01](https://solatcal.com/prayer-times/ngs01) | Negeri Sembilan | Tampin, Jempol | Tampin, Jempol |
| [NGS02](https://solatcal.com/prayer-times/ngs02) | Negeri Sembilan | Kuala Pilah, Rembau, Jelebu | Jelebu, Kuala Pilah, Rembau |
| [NGS03](https://solatcal.com/prayer-times/ngs03) | Negeri Sembilan | Seremban, Nilai, Port Dickson | Port Dickson, Seremban |
| [PHG01](https://solatcal.com/prayer-times/phg01) | Pahang | Pulau Tioman | Pulau Tioman |
| [PHG02](https://solatcal.com/prayer-times/phg02) | Pahang | Kuantan, Pekan, Muadzam Shah | Kuantan, Pekan, Muadzam Shah |
| [PHG03](https://solatcal.com/prayer-times/phg03) | Pahang | Temerloh, Jerantut, Maran | Jerantut, Temerloh, Maran, Bera, Chenor, Jengka |
| [PHG04](https://solatcal.com/prayer-times/phg04) | Pahang | Bentong, Raub, Lipis | Bentong, Lipis, Raub |
| [PHG05](https://solatcal.com/prayer-times/phg05) | Pahang | Bukit Tinggi, Janda Baik, Genting Sempah | Genting Sempah, Janda Baik, Bukit Tinggi |
| [PHG06](https://solatcal.com/prayer-times/phg06) | Pahang | Cameron Highlands, Genting Highlands, Bukit Fraser | Cameron Highlands, Genting Higlands, Bukit Fraser |
| [PHG07](https://solatcal.com/prayer-times/phg07) | Pahang | Rompin, Endau, Pontian (Rompin) | Zon Khas Daerah Rompin, (Mukim Rompin, Mukim Endau, Mukim Pontian) |
| [PRK01](https://solatcal.com/prayer-times/prk01) | Perak | Tanjung Malim, Tapah, Slim River | Tapah, Slim River, Tanjung Malim |
| [PRK02](https://solatcal.com/prayer-times/prk02) | Perak | Ipoh, Kuala Kangsar, Batu Gajah | Kuala Kangsar, Sg. Siput , Ipoh, Batu Gajah, Kampar |
| [PRK03](https://solatcal.com/prayer-times/prk03) | Perak | Gerik, Lenggong, Pengkalan Hulu | Lenggong, Pengkalan Hulu, Grik |
| [PRK04](https://solatcal.com/prayer-times/prk04) | Perak | Temenggor, Belum | Temengor, Belum |
| [PRK05](https://solatcal.com/prayer-times/prk05) | Perak | Teluk Intan, Sitiawan, Lumut | Kg Gajah, Teluk Intan, Bagan Datuk, Seri Iskandar, Beruas, Parit, Lumut, Sitiawan, Pulau Pangkor |
| [PRK06](https://solatcal.com/prayer-times/prk06) | Perak | Taiping, Parit Buntar, Bagan Serai | Selama, Taiping, Bagan Serai, Parit Buntar |
| [PRK07](https://solatcal.com/prayer-times/prk07) | Perak | Bukit Larut | Bukit Larut |
| [PLS01](https://solatcal.com/prayer-times/pls01) | Perlis | Kangar, Arau, Padang Besar | Kangar, Padang Besar, Arau |
| [PNG01](https://solatcal.com/prayer-times/png01) | Pulau Pinang | Penang, George Town, Butterworth | Seluruh Negeri Pulau Pinang |
| [SBH01](https://solatcal.com/prayer-times/sbh01) | Sabah | Sandakan, Sukau, Bukit Garam | Bahagian Sandakan (Timur), Bukit Garam, Semawang, Temanggong, Tambisan, Bandar Sandakan, Sukau |
| [SBH02](https://solatcal.com/prayer-times/sbh02) | Sabah | Beluran, Telupid, Pinangah | Beluran, Telupid, Pinangah, Terusan, Kuamut, Bahagian Sandakan (Barat) |
| [SBH03](https://solatcal.com/prayer-times/sbh03) | Sabah | Lahad Datu, Semporna, Kunak | Lahad Datu, Silabukan, Kunak, Sahabat, Semporna, Tungku, Bahagian Tawau (Timur) |
| [SBH04](https://solatcal.com/prayer-times/sbh04) | Sabah | Tawau, Balong, Merotai | Bandar Tawau, Balong, Merotai, Kalabakan, Bahagian Tawau (Barat) |
| [SBH05](https://solatcal.com/prayer-times/sbh05) | Sabah | Kudat, Kota Marudu, Pitas | Kudat, Kota Marudu, Pitas, Pulau Banggi, Bahagian Kudat |
| [SBH06](https://solatcal.com/prayer-times/sbh06) | Sabah | Gunung Kinabalu | Gunung Kinabalu |
| [SBH07](https://solatcal.com/prayer-times/sbh07) | Sabah | Kota Kinabalu, Penampang, Putatan | Kota Kinabalu, Ranau, Kota Belud, Tuaran, Penampang, Papar, Putatan, Bahagian Pantai Barat |
| [SBH08](https://solatcal.com/prayer-times/sbh08) | Sabah | Keningau, Tambunan, Nabawan | Pensiangan, Keningau, Tambunan, Nabawan, Bahagian Pendalaman (Atas) |
| [SBH09](https://solatcal.com/prayer-times/sbh09) | Sabah | Beaufort, Tenom, Sipitang | Beaufort, Kuala Penyu, Sipitang, Tenom, Long Pasia, Membakut, Weston, Bahagian Pendalaman (Bawah) |
| [SWK01](https://solatcal.com/prayer-times/swk01) | Sarawak | Limbang, Lawas, Sundar | Limbang, Lawas, Sundar, Trusan |
| [SWK02](https://solatcal.com/prayer-times/swk02) | Sarawak | Miri, Marudi, Niah | Miri, Niah, Bekenu, Sibuti, Marudi |
| [SWK03](https://solatcal.com/prayer-times/swk03) | Sarawak | Bintulu, Tatau, Sebauh | Pandan, Belaga, Suai, Tatau, Sebauh, Bintulu |
| [SWK04](https://solatcal.com/prayer-times/swk04) | Sarawak | Sibu, Kapit, Mukah | Sibu, Mukah, Dalat, Song, Igan, Oya, Balingian, Kanowit, Kapit |
| [SWK05](https://solatcal.com/prayer-times/swk05) | Sarawak | Sarikei, Bintangor, Julau | Sarikei, Matu, Julau, Rajang, Daro, Bintangor, Belawai |
| [SWK06](https://solatcal.com/prayer-times/swk06) | Sarawak | Sri Aman, Betong, Saratok | Lubok Antu, Sri Aman, Roban, Debak, Kabong, Lingga, Engkelili, Betong, Spaoh, Pusa, Saratok |
| [SWK07](https://solatcal.com/prayer-times/swk07) | Sarawak | Samarahan, Serian, Simunjan | Serian, Simunjan, Samarahan, Sebuyau, Meludam |
| [SWK08](https://solatcal.com/prayer-times/swk08) | Sarawak | Kuching, Bau, Lundu | Kuching, Bau, Lundu, Sematan |
| [SWK09](https://solatcal.com/prayer-times/swk09) | Sarawak | Kampung Patarikan | Zon Khas (Kampung Patarikan) |
| [SGR01](https://solatcal.com/prayer-times/sgr01) | Selangor | Shah Alam, Petaling Jaya, Subang Jaya | Gombak, Petaling, Sepang, Hulu Langat, Hulu Selangor, S.Alam |
| [SGR02](https://solatcal.com/prayer-times/sgr02) | Selangor | Kuala Selangor, Sabak Bernam | Kuala Selangor, Sabak Bernam |
| [SGR03](https://solatcal.com/prayer-times/sgr03) | Selangor | Klang, Port Klang, Banting | Klang, Kuala Langat |
| [TRG01](https://solatcal.com/prayer-times/trg01) | Terengganu | Kuala Terengganu, Kuala Nerus, Marang | Kuala Terengganu, Marang, Kuala Nerus |
| [TRG02](https://solatcal.com/prayer-times/trg02) | Terengganu | Besut, Setiu | Besut, Setiu |
| [TRG03](https://solatcal.com/prayer-times/trg03) | Terengganu | Hulu Terengganu | Hulu Terengganu |
| [TRG04](https://solatcal.com/prayer-times/trg04) | Terengganu | Kemaman, Dungun | Dungun, Kemaman |
| [WLY01](https://solatcal.com/prayer-times/wly01) | Wilayah Persekutuan | Kuala Lumpur, Putrajaya | Kuala Lumpur, Putrajaya |
| [WLY02](https://solatcal.com/prayer-times/wly02) | Wilayah Persekutuan | Labuan | Labuan |

The same list as JSON is at `https://solatcal.com/api/zones`. Codes are three letters for the
state (JHR Johor, KDH Kedah, KTN Kelantan, MLK Melaka, NGS Negeri Sembilan,
PHG Pahang, PLS Perlis, PNG Pulau Pinang, PRK Perak, SBH Sabah, SGR Selangor,
SWK Sarawak, TRG Terengganu, WLY Wilayah Persekutuan) plus a two digit number.

## Attribution

Prayer times are the property of JAKIM e-Solat: https://www.e-solat.gov.my/index.php?siteId=24&pageId=24
SolatCal only reformats them, and is not affiliated with or endorsed by JAKIM.
Made by [Umar Rasydan](https://github.com/umarrasydan).
