{"openapi":"3.1.0","info":{"title":"SolatCal API","version":"2.1.0","summary":"JAKIM e-Solat prayer times for Malaysia as a calendar feed and JSON.","description":"Official JAKIM e-Solat prayer times for all 60 Malaysian zones, unchanged, as an iCalendar feed any calendar app can subscribe to, plus JSON endpoints. Free, with no key and no account. Every /api/* path and /mcp allow 300 requests a minute per client IP, stated on each response in RateLimit-Policy and X-RateLimit-Limit. Times are the property of JAKIM e-Solat (https://www.e-solat.gov.my/index.php?siteId=24&pageId=24); SolatCal is unofficial and not affiliated with JAKIM.","license":{"name":"MIT","identifier":"MIT"}},"externalDocs":{"description":"API and agent docs","url":"https://solatcal.com/docs"},"servers":[{"url":"https://solatcal.com"}],"security":[],"paths":{"/api/feed.ics":{"get":{"operationId":"getFeed","summary":"Calendar feed for one zone","description":"An iCalendar feed of prayer times. Subscribe to the URL for a feed that updates itself (use the webcal: scheme for Apple Calendar), or add download=1 for a one-off file.","parameters":[{"name":"zone","in":"query","description":"JAKIM zone code. See /api/zones.","schema":{"type":"string","enum":["JHR01","JHR02","JHR03","JHR04","KDH01","KDH02","KDH03","KDH04","KDH05","KDH06","KDH07","KTN01","KTN02","MLK01","NGS01","NGS02","NGS03","PHG01","PHG02","PHG03","PHG04","PHG05","PHG06","PHG07","PLS01","PNG01","PRK01","PRK02","PRK03","PRK04","PRK05","PRK06","PRK07","SBH01","SBH02","SBH03","SBH04","SBH05","SBH06","SBH07","SBH08","SBH09","SGR01","SGR02","SGR03","SWK01","SWK02","SWK03","SWK04","SWK05","SWK06","SWK07","SWK08","SWK09","TRG01","TRG02","TRG03","TRG04","WLY01","WLY02"]},"required":true,"example":"WLY01"},{"name":"prayers","in":"query","description":"Comma separated prayers to include, any of fajr, syuruk, dhuhr, asr, maghrib, isha.","schema":{"type":"string","pattern":"^(fajr|syuruk|dhuhr|asr|maghrib|isha)(,(fajr|syuruk|dhuhr|asr|maghrib|isha))*$","default":"fajr,dhuhr,asr,maghrib,isha"}},{"name":"align","in":"query","description":"Event start: at the solat time, 5 or 10 minutes before, or snapped down to the containing 15 minute block.","schema":{"type":"string","enum":["exact","early5","early10","block15"],"default":"exact"}},{"name":"duration","in":"query","description":"Event length in minutes. With block15 the end also rounds up to the next quarter hour.","schema":{"type":"integer","minimum":5,"maximum":120,"default":15}},{"name":"busy","in":"query","description":"Show events as busy (TRANSP:OPAQUE) or free (TRANSPARENT).","schema":{"type":"string","enum":["busy","free"],"default":"busy"}},{"name":"alarm","in":"query","description":"Display reminder, that many minutes before the start.","schema":{"type":"string","enum":["none","0","5","10"],"default":"none"}},{"name":"ramadan","in":"query","description":"With 1, Ramadan days gain an Imsak event from imsak to Subuh, and Maghrib is titled as iftar.","schema":{"type":"string","enum":["0","1"],"default":"0"}},{"name":"hijri","in":"query","description":"All-day hijri date events, always free: major occasions only, or every day.","schema":{"type":"string","enum":["none","major","daily"],"default":"none"}},{"name":"lang","in":"query","description":"Language of the labels inside the calendar. Prayer names are Malay either way.","schema":{"type":"string","enum":["en","ms"],"default":"en"}},{"name":"period","in":"query","description":"Date range for a one-off download: month and quarter run from today, year to 31 December. Subscriptions always carry the rest of the year.","schema":{"type":"string","enum":["month","quarter","year"],"default":"year"}},{"name":"download","in":"query","description":"With 1, adds Content-Disposition: attachment.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The calendar. X-Solat-Source is jakim-stale-cache when JAKIM has not answered the daily check for over three days.","content":{"text/calendar":{"schema":{"type":"string"}}}},"400":{"description":"A parameter is missing or has a value the API does not accept.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JAKIM e-Solat returned nothing usable for this zone and range.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/preview":{"get":{"operationId":"getPreview","summary":"Today's events for one zone, as JSON","description":"Takes the same parameters as the feed and applies the same rules.","parameters":[{"name":"zone","in":"query","description":"JAKIM zone code. See /api/zones.","schema":{"type":"string","enum":["JHR01","JHR02","JHR03","JHR04","KDH01","KDH02","KDH03","KDH04","KDH05","KDH06","KDH07","KTN01","KTN02","MLK01","NGS01","NGS02","NGS03","PHG01","PHG02","PHG03","PHG04","PHG05","PHG06","PHG07","PLS01","PNG01","PRK01","PRK02","PRK03","PRK04","PRK05","PRK06","PRK07","SBH01","SBH02","SBH03","SBH04","SBH05","SBH06","SBH07","SBH08","SBH09","SGR01","SGR02","SGR03","SWK01","SWK02","SWK03","SWK04","SWK05","SWK06","SWK07","SWK08","SWK09","TRG01","TRG02","TRG03","TRG04","WLY01","WLY02"]},"required":true,"example":"WLY01"},{"name":"prayers","in":"query","description":"Comma separated prayers to include, any of fajr, syuruk, dhuhr, asr, maghrib, isha.","schema":{"type":"string","pattern":"^(fajr|syuruk|dhuhr|asr|maghrib|isha)(,(fajr|syuruk|dhuhr|asr|maghrib|isha))*$","default":"fajr,dhuhr,asr,maghrib,isha"}},{"name":"align","in":"query","description":"Event start: at the solat time, 5 or 10 minutes before, or snapped down to the containing 15 minute block.","schema":{"type":"string","enum":["exact","early5","early10","block15"],"default":"exact"}},{"name":"duration","in":"query","description":"Event length in minutes. With block15 the end also rounds up to the next quarter hour.","schema":{"type":"integer","minimum":5,"maximum":120,"default":15}},{"name":"busy","in":"query","description":"Show events as busy (TRANSP:OPAQUE) or free (TRANSPARENT).","schema":{"type":"string","enum":["busy","free"],"default":"busy"}},{"name":"alarm","in":"query","description":"Display reminder, that many minutes before the start.","schema":{"type":"string","enum":["none","0","5","10"],"default":"none"}},{"name":"ramadan","in":"query","description":"With 1, Ramadan days gain an Imsak event from imsak to Subuh, and Maghrib is titled as iftar.","schema":{"type":"string","enum":["0","1"],"default":"0"}},{"name":"hijri","in":"query","description":"All-day hijri date events, always free: major occasions only, or every day.","schema":{"type":"string","enum":["none","major","daily"],"default":"none"}},{"name":"lang","in":"query","description":"Language of the labels inside the calendar. Prayer names are Malay either way.","schema":{"type":"string","enum":["en","ms"],"default":"en"}}],"responses":{"200":{"description":"Today's events.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Preview"}}}},"400":{"description":"A parameter is missing or has a value the API does not accept.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JAKIM e-Solat returned nothing usable for this zone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/zones":{"get":{"operationId":"listZones","summary":"Every JAKIM zone","responses":{"200":{"description":"Zone codes and their districts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Zones"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/places":{"get":{"operationId":"listPlaces","summary":"Towns and districts per zone, with coordinates","description":"What the pages use to find a zone by a place name or by the visitor's position.","responses":{"200":{"description":"Places by zone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Places"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/find":{"get":{"operationId":"findZone","summary":"The zone for a town, district, state or position","description":"JAKIM names districts, not towns. This finds the zone for any of 524 towns and areas, short forms such as KL, PJ, JB and KK, abbreviations such as Sg and Bt, or a position. Give q, or lat and lon.","parameters":[{"name":"q","in":"query","description":"Place name, for example Subang Jaya or \"Shah Alam, Selangor\".","schema":{"type":"string"},"examples":{"town":{"value":"Subang Jaya"}}},{"name":"lat","in":"query","description":"Latitude in decimal degrees, with lon.","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lon","in":"query","description":"Longitude in decimal degrees, with lat.","schema":{"type":"number","minimum":-180,"maximum":180}}],"responses":{"200":{"description":"Matches, best first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Find"}}}},"400":{"description":"Neither q nor lat and lon were given, or the position is not a number.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/health":{"get":{"operationId":"getHealth","summary":"Service and upstream status","responses":{"200":{"description":"Status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/mcp":{"post":{"operationId":"mcp","summary":"Model Context Protocol server","description":"Stateless Streamable HTTP MCP server speaking JSON-RPC 2.0. With protocol version 2026-07-28, every request carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta and the headers below, and the methods are server/discover, tools/list and tools/call. The earlier versions (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05) open with initialize and also accept ping and notifications. Tools: find_zone, list_zones, get_prayer_times, get_calendar_feed. One request per POST, at most 32 KB.","parameters":[{"name":"MCP-Protocol-Version","in":"header","description":"Required with 2026-07-28, and must match params._meta. Earlier versions send the version initialize agreed.","schema":{"type":"string","enum":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"]}},{"name":"Mcp-Method","in":"header","description":"Required with 2026-07-28: the JSON-RPC method.","schema":{"type":"string"}},{"name":"Mcp-Name","in":"header","description":"Required on tools/call with 2026-07-28: the tool name, as plain ASCII or =?base64?...?=.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"const":"2.0"},"id":{"type":["string","integer"]},"method":{"type":"string"},"params":{"type":"object","properties":{"_meta":{"type":"object","description":"Required with 2026-07-28.","properties":{"io.modelcontextprotocol/protocolVersion":{"type":"string"},"io.modelcontextprotocol/clientCapabilities":{"type":"object"},"io.modelcontextprotocol/clientInfo":{"type":"object"}}}}}}},"example":{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}}}},"responses":{"200":{"description":"A JSON-RPC response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"202":{"description":"A notification was accepted. No body."},"400":{"description":"The body is not JSON or is a batch. Or, for a request that names a version: a required _meta field is missing (-32602), a header disagrees with the body (-32020), or the version is not supported (-32022).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"404":{"description":"With 2026-07-28: the method does not exist (-32601).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"413":{"description":"The body is too large.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JsonRpcResponse"}}}},"429":{"description":"More than 300 requests in a minute from one client. Retry after the number of seconds in Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"JsonRpcResponse":{"type":"object","required":["jsonrpc","id"],"description":"A JSON-RPC 2.0 response: either result or error.","properties":{"jsonrpc":{"const":"2.0"},"id":{"type":["string","integer","null"]},"result":{"type":"object","description":"The method result, such as { tools: [...] } or a tool call result with content."},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"integer","description":"-32700 parse error, -32600 invalid request, -32601 unknown method, -32602 unknown tool or missing _meta field, -32603 internal error, -32020 headers disagree with the body, -32022 unsupported protocol version."},"message":{"type":"string"},"data":{"type":"object","description":"With -32022: the versions this server supports and the one that was requested.","properties":{"supported":{"type":"array","items":{"type":"string"}},"requested":{"type":"string"}}}}}}},"Error":{"type":"object","required":["error","status"],"properties":{"error":{"type":"string","description":"What was wrong, naming the parameter if there was one."},"status":{"type":"integer"}}},"Preview":{"type":"object","description":"One day of a feed: today's events after the same align and duration rules as the ICS feed.","properties":{"zone":{"type":"string"},"description":{"type":"string","description":"JAKIM's district list for the zone."},"date":{"type":"string","format":"date"},"dateLabel":{"type":"string"},"hijri":{"type":["string","null"],"description":"Hijri date as JAKIM publishes it, YYYY-MM-DD."},"hijriLabel":{"type":"string"},"ramadan":{"type":"boolean"},"imsak":{"oneOf":[{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM, Malaysia time (UTC+08:00)."},{"type":"null"}]},"events":{"type":"array","items":{"type":"object","properties":{"prayer":{"type":"string","enum":["fajr","syuruk","dhuhr","asr","maghrib","isha","imsak"]},"name":{"type":"string","description":"Malay prayer name."},"summary":{"type":"string","description":"Event title, such as Jumaat on Fridays."},"solat":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM, Malaysia time (UTC+08:00)."},"start":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM, Malaysia time (UTC+08:00)."},"end":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM, Malaysia time (UTC+08:00)."},"crossesMidnight":{"type":"boolean"}}}},"allDay":{"type":"array","items":{"type":"object","properties":{"summary":{"type":"string"},"occasion":{"type":["string","null"]}}}},"tomorrow":{"description":"Tomorrow's raw times. Null on 31 December, as JAKIM publishes one year at a time.","oneOf":[{"type":"object","properties":{"date":{"type":"string","format":"date"},"times":{"type":"object","additionalProperties":{"type":"string","pattern":"^\\d{2}:\\d{2}$","description":"HH:MM, Malaysia time (UTC+08:00)."}}}},{"type":"null"}]},"stale":{"type":"boolean","description":"True when JAKIM has not answered the daily check for over three days, so the stored copy may be out of date."}}},"Zones":{"type":"object","description":"Every zone code mapped to the districts JAKIM lists for it.","additionalProperties":{"type":"string"}},"Places":{"type":"object","properties":{"zones":{"type":"object","additionalProperties":{"type":"object","properties":{"state":{"type":"string"},"description":{"type":"string"},"name":{"type":"array","items":{"type":"string"},"description":"What the zone is known as, most searched first."},"places":{"type":"array","description":"Towns and districts as [name, latitude, longitude].","items":{"type":"array","prefixItems":[{"type":"string"},{"type":"number"},{"type":"number"}]}},"radius":{"type":"number","description":"For small special zones: kilometres within which a position belongs to them."},"headline":{"type":"array","items":{"type":"string"},"description":"The towns page titles lead with, where they differ from name."},"aliases":{"type":"array","description":"Short forms and other spellings as [alias, place], for example [\"KL\", \"Kuala Lumpur\"].","items":{"type":"array","prefixItems":[{"type":"string"},{"type":"string"}]}}}}},"states":{"type":"object","additionalProperties":{"type":"string"}}}},"Find":{"type":"object","required":["matches"],"properties":{"query":{"type":"string"},"latitude":{"type":"number"},"longitude":{"type":"number"},"matches":{"type":"array","description":"Best match first. Empty when nothing matches, with a hint.","items":{"type":"object","properties":{"zone":{"type":"string","examples":["SGR01"]},"state":{"type":"string"},"areas":{"type":"string","description":"The places the zone is best known for."},"districts":{"type":"string","description":"JAKIM's own description of the zone."},"place":{"type":["string","null"],"description":"The town or district that matched, or null for a zone code or state."},"match":{"type":"string","enum":["place","code","state","nearest"]},"km":{"type":"number","description":"For a position: distance to the nearest known place."},"page":{"type":"string","format":"uri"},"pageMs":{"type":"string","format":"uri"},"today":{"type":"string","format":"uri","description":"Today's times for the zone (/api/preview)."},"feed":{"type":"string","format":"uri","description":"The calendar feed for the zone."}}}},"hint":{"type":"string"}}},"Health":{"type":"object","properties":{"ok":{"type":"boolean"},"upstream":{"type":"string","enum":["ok","stale-cache","unreachable","unknown"]},"upstreamUrl":{"type":"string","format":"uri"},"sampleDays":{"type":"integer"},"zones":{"type":"integer"},"serverTime":{"type":"string","format":"date-time"}}}}}}