A small Go microservice implementing a subset of the Hebcal.com REST APIs, ported from the Node.js implementation in hebcal-web. It reimplements the JSON/XML APIs in Go for higher throughput and lower latency.
Currently implemented:
- Hebrew Date Converter (JSON and XML) — ported from
src/converter.js - Zmanim (halachic times, JSON) — ported from
src/zmanim.js - Assur Melacha ("is work prohibited right now", JSON) — the
im=1mode of the zmanim API - Shabbat (candle-lighting / Torah portion, JSON) — ported from
src/shabbat.js - Geolocation (
/geo) — resolve query parameters to a location, JSON - Geo autocomplete (
/complete) — city/ZIP typeahead, JSON
Date conversions use hebcal/hdate
(FromProlepticGregorian, matching JavaScript Date behavior); holidays,
parshiyot and zmanim come from
hebcal/hebcal-go (v0.16.2+), whose
solar calculations are backed by hebcal/noaa-go.
GET|POST|HEAD /converter?cfg=json|xml&…— Gregorian ⇄ Hebrew date conversion. Thecfgparameter is required and must bejsonorxml(400 otherwise). POST requests are accepted, but any request body is ignored; conversion parameters always come from the URL query string.g2h=1withdate=YYYY-MM-DDorgy/gm/gd(+ optionalgs=onfor after sunset)h2g=1withhy/hm/hd(+ optionalndays=2..399for a batch)start=YYYY-MM-DD&end=YYYY-MM-DDfor a batch of Gregorian dates (cfg=json only, truncated to 399 days)strict=1,i=on,lg=<lang>,callback=<fn>as documented- If no date is given, the current date in
America/New_Yorkis used (and the response is marked non-cacheable).
GET|HEAD /converter/csv?…— CSV download listing the Gregorian dates of the given Hebrew calendar date from 5 years before to 75 years after.
GET|HEAD /zmanim?cfg=json&…— halachic times for a location and date.cfg=jsonis required. Requires the geonames/zips databases (see Location databases); without them this route returns 503 while the other APIs keep working.- Date:
date=YYYY-MM-DDfor a single day, orstart=YYYY-MM-DD&end=YYYY-MM-DDfor a range (capped at 399 days). If omitted, "today" in the location's timezone is used. - Location — one of (see Location resolution):
geonameid,zip,city, decimallatitude+longitude+tzid, or the legacyladeg/lamin/ladir+lodeg/lomin/lodirform. ue=1includes the location's elevation in sunrise/sunset (and the times derived from them); theseaLevelSunrise/seaLevelSunsettimes are only present when elevation is enabled.sec=1returns seconds instead of rounding each time to the minute.- Times that do not occur on a given day (e.g. no astronomical dawn in
the polar summer) are returned as
null.
- Date:
GET|HEAD /zmanim?cfg=json&im=1&…— Assur Melacha check: whether melacha (work) is prohibited at a given instant (Shabbat or Yom Tov). Same location parameters as above.dt=<ISO 8601>selects the instant (a bareYYYY-MM-DDis UTC midnight; a datetime without a zone is interpreted in the location's timezone; a trailingZor±HH:MMoffset is honored). Ifdtis omitted the current time is used and the response is cached for 60s.
-
GET|HEAD /geo?…— resolve a location from query parameters and return the location as JSON (ported from the/georoute in hebcal-web'ssrc/router.js).OPTIONSreturns a CORS preflight; other methods return405. Accepts the same location parameters as/zmanim(see Location resolution), and returns the raw@hebcal/coreLocation shape:{"latitude":31.76904,"longitude":35.21633,"locationName":"Jerusalem, Israel","timeZoneId":"Asia/Jerusalem","elevation":786,"il":true,"cc":"IL","geoid":281184,"admin1":"Jerusalem District","geo":"geoname","population":801000,"asciiname":"Jerusalem","geonameid":281184}This differs from the trimmed
locationobject embedded in the/zmanimand/shabbatresponses (different key names, and it always includeselevation,il,geoidandpopulation). A request with no location parameters returns204 No Content; an unknowngeonameid/zip/cityreturns404, and malformed input returns400. Requires the geonames/zips databases (503 otherwise).
-
GET /complete?q=<prefix>(also/complete.php) — city and US-ZIP typeahead, ported from hebcal-web'ssrc/complete.js. Returns a JSON array of up to 12 matches, each with a country-flag emoji. A leading digit is treated as a ZIP code (exact 5-digit or numeric prefix); otherwise both the geonames and US-ZIP full-text indexes are searched, merged (GeoNames winning ties), and sorted by population.g=on(org=1) additionally returnslatitude/longitude/timezone/population.- An empty
qor no matches returns404 {"error":"Not Found"}. - Responses are cached for 3 days with a weak
ETag.
[{"id":281184,"value":"Jerusalem, Israel","admin1":"Jerusalem District","country":"Israel","cc":"IL","geo":"geoname","asciiname":"Jerusalem","flag":"🇮🇱"}]The full-text queries use SQLite FTS5, so the
mattn/go-sqlite3driver must be built with thesqlite_fts5tag (theMakefileand CI already pass-tags sqlite_fts5).
GET /ping— health check. Serves the contents of/var/www/html/ping(override with-pingfile) astext/plain, the same file hebcal-web serves; returns 404 when the file is absent, so removing it takes the host out of load-balancer rotation.GET /metrics— Prometheus metrics, includinghttp_requests_total.
The /zmanim API accepts the same location parameters as hebcal-web, in
this order of precedence:
geonameid=<id>— a GeoNames numeric id.zip=<5-digit>— a US ZIP code.city=<id>— a legacy Hebcal city identifier (e.g.GB-London).latitude=<deg>&longitude=<deg>&tzid=<IANA tz>— decimal degrees, with south/west expressed as negative numbers.elev=<meters>is optional (used only withue=1), andi=onselects the Israel schedule.ladeg/lamin/ladir+lodeg/lomin/lodir— the legacy degree/minute/direction form, where south/west are positive magnitudes with a direction letter (s/w). A legacytz/dstpair is mapped to an IANA timezone whentzidis absent.
Unlike hebcal-web, this service does not guess a timezone from
latitude/longitude shape data, so tzid (or a resolvable tz/dst) is
required for the positional forms. GeoIP-based location is also out of
scope.
Location resolution reads two prebuilt SQLite databases,
geonames.sqlite3 and zips.sqlite3, produced by
@hebcal/geo-sqlite. Their paths
default to the working directory and can be set with the -zips-db /
-geonames-db flags or the ZIPS_DB / GEONAMES_DB environment
variables. Small sample databases used by the tests live in testdata/.
Responses include weak ETag validators (FNV-1a; the Node.js service uses
murmurhash3 — weak ETags do not need to match across implementations),
appropriate Cache-Control or Expires headers, CORS headers, and
dynamic brotli or gzip compression (brotli preferred) for bodies larger
than 512 bytes — a threshold chosen empirically: multi-day batches and
event-heavy XML just above it shrink 40–60%, while typical single-date
JSON below it saves almost nothing (see TestThresholdExperiment).
Zmanim caching mirrors hebcal-web: a single live date expires at the next
local midnight; an explicit date or range is cached for 30 days with an
ETag; the live Assur Melacha check is cached for 60 seconds.
- Same-day events may appear in a slightly different order within the
eventsarray. - Molad announcements are rendered in hebcal-go's format rather than @hebcal/core's.
strict=1validation errors return a clean{"error": "..."}object without the stack trace that koa-error appends in development mode.- Zmanim times agree with @hebcal/core to within ~2 seconds (the inherent difference between the noaa-go and @hebcal/core NOAA implementations); minute-rounded output matches except where a value falls within 2s of a rounding boundary.
Requires Go >= 1.24 and cgo (a C compiler), because the location
lookups use the cgo-based github.com/mattn/go-sqlite3 driver. The driver
must be built with the sqlite_fts5 tag so the /complete full-text
queries work; the Makefile targets pass it for you.
make build # builds ./hebcal-api (CGO_ENABLED=1 -tags sqlite_fts5)
make test # runs the unit tests (-tags sqlite_fts5)If you invoke go directly rather than through the Makefile, add the
tag yourself, e.g. go test -tags sqlite_fts5 ./....
./hebcal-api # listens on :8082, logs to stdout
./hebcal-api -port 8082 -logfile /var/log/hebcal/api.log \
-zips-db /var/lib/hebcal/zips.sqlite3 \
-geonames-db /var/lib/hebcal/geonames.sqlite3The port defaults to 8082 (or the PORT environment variable); the
access log defaults to stdout (pass -logfile <path>). The geonames/zips
database paths default to the working directory (see
Location databases).
Access logs are pino-compatible JSON lines, e.g.:
{"level":30,"time":1783224620662,"pid":46493,"hostname":"w44","status":200,"length":217,"duration":1,"ip":"1.2.3.4","method":"GET","url":"/converter?cfg=json&gy=2026&gm=7&gd=4&g2h=1","ua":"curl/8.5.0"}Sending SIGUSR1 (or SIGHUP) makes the server close and reopen the
access log file, for use with logrotate.
sudo make install # installs binary, systemd unit, logrotate config
sudo systemctl start hebcal-apimake install installs the binary to /usr/local/bin, the systemd unit
to /etc/systemd/system/hebcal-api.service, and the logrotate drop-in to
/etc/logrotate.d/hebcal-api. The service runs as www-data and writes
its access log to /var/log/hebcal/api.log (same directory hebcal-web
uses), rotated daily; logrotate signals the service with SIGUSR1 to
reopen the file after rotation.