Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

69 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hebcal-api

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=1 mode of the zmanim API
  • Shabbat (candle-lighting / Torah portion, JSON) — ported from src/shabbat.js; Torah readings come from the readings-svc sidecar (see Torah readings)
  • Geolocation (/geo) — resolve query parameters to a location, JSON
  • Geo autocomplete (/complete) — city/ZIP typeahead, JSON
  • PDF calendars — the download.hebcal.com/v4/…pdf downloads (ported from src/pdf.js) and www.hebcal.com/holidays/hebcal-<year>.pdf (ported from src/holidayPdf.js). These came from the separate hebcal-pdf-go service, which this repository replaces.

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.

Endpoints

Hebrew Date Converter

  • GET|POST|HEAD /converter?cfg=json|xml&… — Gregorian ⇄ Hebrew date conversion. The cfg parameter is required and must be json or xml (400 otherwise). POST requests are accepted, but any request body is ignored; conversion parameters always come from the URL query string.
    • g2h=1 with date=YYYY-MM-DD or gy/gm/gd (+ optional gs=on for after sunset)
    • h2g=1 with hy/hm/hd (+ optional ndays=2..399 for a batch)
    • start=YYYY-MM-DD&end=YYYY-MM-DD for 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_York is 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.

Zmanim

  • GET|HEAD /zmanim?cfg=json&… — halachic times for a location and date. cfg=json is 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-DD for a single day, or start=YYYY-MM-DD&end=YYYY-MM-DD for 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, decimal latitude+longitude+tzid, or the legacy ladeg/lamin/ladir + lodeg/lomin/lodir form.
    • ue=1 includes the location's elevation in sunrise/sunset (and the times derived from them); the seaLevelSunrise/seaLevelSunset times are only present when elevation is enabled.
    • sec=1 returns 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.
  • 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 bare YYYY-MM-DD is UTC midnight; a datetime without a zone is interpreted in the location's timezone; a trailing Z or ±HH:MM offset is honored). If dt is omitted the current time is used and the response is cached for 60s.

Shabbat

  • GET|HEAD /shabbat?cfg=json&… — candle-lighting, the weekly Torah portion, havdalah and the other events of one Shabbat week for a location, ported from src/shabbat.js. cfg=json is required (501 otherwise). OPTIONS returns a CORS preflight; other methods return 405. Requires the geonames/zips databases (503 otherwise).
    • Date — the week containing the given day, in this order of precedence: dt=YYYY-MM-DD, date=YYYY-MM-DD, start=YYYY-MM-DD (end is accepted but ignored), or gy/gm/gd. With none of them, "now" in the location's timezone is used and the response expires at the end of Saturday rather than being cached for 7 days. The window runs from that day (backing up to Friday when it is a Saturday, so last night's candle-lighting is included) through the later of the upcoming Saturday or five days ahead.
    • Location — the same parameters as /zmanim (see Location resolution). Defaults to New York when none is given.
    • b=<min> sets candle-lighting minutes before sunset; b=0 lights at sunset itself. The default is 18, or the local custom in Israel (40 in Jerusalem, 30 in Haifa and Zikhron Ya'akov, 20 elsewhere) — which also replaces the b=18 the web form submits when the reader expressed no preference.
    • M=on or td=<deg> ends Shabbat at a solar depression angle (default 8.5°) and m=<min> at a fixed number of minutes after sunset; m=0 suppresses havdalah entirely. When more than one is given, td wins over m, M=on wins over both, and M=off picks m over td.
    • ue=on folds the location's elevation into sunrise and sunset.
    • i=on puts a Diaspora location on the Israel schedule (the candle-lighting custom still follows the location itself).
    • molad=on adds the molad announcement on Shabbat Mevarchim, with the exact moment of the conjunction as a UTC instant.
    • yto=on keeps only the Yom Tov days; a week without one returns an empty items array.
    • h12=0 forces a 24-hour clock and h12=1 a 12-hour one, overriding the location's country.
    • lg=<lang> translates the event titles (a=on is the much older spelling of lg=a); an unsupported locale returns 400 {"error":"Locale 'xx' not found"}, as hebcal-web does here. hdp=1 adds heDateParts.
    • leyning=off (or leyning=0) omits the Torah readings; see below.
    • callback=<fn> wraps the response in a JSONP call. A callback longer than 128 characters or that is not a plain dotted identifier is ignored, and ordinary JSON is returned.

Torah readings

Each non-timed item carries a leyning object — the aliyot, torah summary, haftarah (plus the haftarah_sephardic and haftarah_chabad variants where they differ), maftir, and, for a parsha, the triennial cycle:

{"1":"Deuteronomy 11:26-12:10","…":"","torah":"Deuteronomy 11:26-16:17",
 "haftarah":"Isaiah 54:11-55:5","maftir":"Deuteronomy 16:13-16:17",
 "triennial":{"1":"Deuteronomy 11:26-11:31","…":""}}

hebcal-go has no leyning data, so the readings come from the readings-svc sidecar's /leyning endpoint, over a unix domain socket set with -readings-socket or READINGS_SOCKET (default /run/hebcal/readings-svc.sock). That service answers in @hebcal/rest-api's classic-API shape, so each reading is passed through into the response verbatim, key order and all, rather than being reformatted here.

Readings depend only on the date and on Israel vs. Diaspora — every city in Israel reads the same portion on a given day, as does every city in the Diaspora — so they are cached in a 400-entry LRU keyed by (date, il), which a week's worth of requests for any location shares. /leyning takes no locale at all: the readings are locale-invariant, and items are matched to hebcal-go's events by the untranslated event description, so they stay English however lg was set. When readings-svc cannot be reached, /shabbat returns 503 rather than a response that silently omits the readings; leyning=off skips the call altogether.

Geolocation

  • GET|HEAD /geo?… — resolve a location from query parameters and return the location as JSON (ported from the /geo route in hebcal-web's src/router.js). OPTIONS returns a CORS preflight; other methods return 405. Accepts the same location parameters as /zmanim (see Location resolution), and returns the raw @hebcal/core Location 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 location object embedded in the /zmanim and /shabbat responses (different key names, and it always includes elevation, il, geoid and population). A request with no location parameters returns 204 No Content; an unknown geonameid/zip/city returns 404, and malformed input returns 400. Requires the geonames/zips databases (503 otherwise).

Geo autocomplete

  • GET /complete?q=<prefix> (also /complete.php) — city and US-ZIP typeahead, ported from hebcal-web's src/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 (or g=1) additionally returns latitude/longitude/timezone/population.
    • An empty q or no matches returns 404 {"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-sqlite3 driver must be built with the sqlite_fts5 tag (the Makefile and CI already pass -tags sqlite_fts5).

PDF calendars

Each calendar is US Letter, landscape, one Gregorian or Hebrew month per page, with holidays, candle-lighting and Havdalah times, Torah readings, daily-learning schedules, the Omer, alternate (Hebrew or Gregorian) dates, and tracked links over every event. Hebrew and other right-to-left text is laid out with real Unicode bidi and OpenType shaping, and event titles are available in 13 locales. Rendering needs the fonts (see Fonts); without them these two routes answer 503 and the JSON APIs are unaffected.

  • GET|HEAD /v4/<base64>/<filename>.pdf — render a calendar. The <base64> segment is a Download protobuf message (defined in pkg/downloadpb/download.proto, shared with hebcal-web) carrying the calendar options: year or month range, location, event categories, locale, candle-lighting and Havdalah preferences, daily-learning series, and so on. These URLs are produced by the hebcal.com download form. A request whose options select no events returns 400; an unknown location returns 404; a year outside the supported range returns 410. A rendered PDF is served with a 14-day Cache-Control and a weak ETag; a conditional request whose If-None-Match still matches gets 304 Not Modified.
  • GET|HEAD /v2/h/<base64>/<filename>.pdf — the same calendar from the older URL form, still linked from a decade of pages and still crawled. Here the <base64> segment is a query string rather than a protobuf (v=1&geonameid=5128581&year=2026&c=on&…). hebcal-web answers these with a 301 to the /v4/ form; this service converts the query string to the same Download message that redirect would have carried and renders it, so the response is a 200 with the identical calendar and headers. Two location forms downloadHref2() has no branch for — a legacy city= identifier and the ladeg/lamin/ladir degrees-and-minutes form — are resolved rather than dropped, which is what these URLs rendered before hebcal-web's redirect was added. Only .pdf under /v2/h/ is served: the .ics feeds and the yahrzeit calendars sharing that prefix get 404, as does a URL with no v=.
  • GET|HEAD /holidays/hebcal-<year>.pdf — render a year of Jewish holidays, the calendar www.hebcal.com links from its holiday pages. A year of 3761 or more is a Hebrew year, as is the Gregorian-span form the year-index pages use, hebcal-2026-2027.pdf; anything smaller is a Gregorian year. ?i=on selects the Israel schedule, and that is the only parameter: these calendars are always English, as every link to them on the website is. Every day carries its Hebrew date, and there are no candle-lighting times, since they have no location. A URL that is not a holiday calendar returns 404, a year outside 1..32000 returns 400, and a year with no calendar returns 410. A rendered PDF is served with a 60-day Cache-Control — four times the download path's, since these depend on nothing but the year — and the same weak ETag and 304 handling. Nothing else under /holidays/ belongs to this service: the HTML pages there are still hebcal-web's, and this route answers them 404.

Daily learning

hebcal-web offers 20 daily-learning series. Thirteen of them — Daf Yomi, Mishna Yomi, Nach Yomi, the two Yerushalmi editions, Perek Yomi, Daf-a-Week, 929, Psalms, the two Rambam schedules, Tanakh Yomi and Pirkei Avot — are generated in-process by hebcal/learning. The remaining six — Sefer HaMitzvot, Kitzur Shulchan Arukh, Arukh HaShulchan, Amud HaYomi (Dirshu), Chofetz Chaim and Shemirat HaLashon — are fetched from the readings-svc sidecar's /learning endpoint and merged into the calendar, so every series still renders. Each learning row links to its source on Sefaria (or dafyomi.org).

-readings-socket (or READINGS_SOCKET) is the same socket /shabbat uses for Torah readings; the two endpoints are served by one process.

Neither failure mode serves a calendar quietly missing rows the reader asked for; both name the missing series in an X-Unsupported-Series header (which the access log records as unsupported):

  • 501 when -readings-socket is empty — this build cannot render those series and retrying will not help (unreachable in the default configuration).
  • 503, with Retry-After, when a configured readings-svc does not answer — transient, and worth retrying or falling back to the Node.js service.

Fonts

The calendars are drawn with the Source Sans Pro and Adobe Hebrew families, checked into this repository under fonts/. -fonts (or FONT_DIR, default fonts) names a directory holding Source_Sans_Pro/ and Adobe_Hebrew/. The fonts are parsed once at startup and shared by every request; only the per-document embedded instances are rebuilt. A failure to load them is logged and disables the two PDF routes rather than stopping the server.

Operational

  • GET /ping — health check. Serves the contents of /var/www/html/ping (override with -pingfile) as text/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, including http_requests_total.

Location resolution

The /zmanim API accepts the same location parameters as hebcal-web, in this order of precedence:

  1. geonameid=<id> — a GeoNames numeric id.
  2. zip=<5-digit> — a US ZIP code.
  3. city=<id> — a legacy Hebcal city identifier (e.g. GB-London).
  4. latitude=<deg>&longitude=<deg>&tzid=<IANA tz> — decimal degrees, with south/west expressed as negative numbers. elev=<meters> is optional (used only with ue=1), and i=on selects the Israel schedule.
  5. 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 legacy tz/dst pair is mapped to an IANA timezone when tzid is 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 databases

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

Caching and compression

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.

Known differences from the Node.js implementation

  • Same-day events may appear in a slightly different order within the events array.
  • Molad announcements are rendered in hebcal-go's format rather than @hebcal/core's.
  • strict=1 validation 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.
  • /shabbat honors date= and start= as ways of pinning the week; hebcal-web reads only dt= and gy/gm/gd there and quietly falls back to today for the others.
  • /shabbat with yto=on and no Yom Tov in the week returns 200 and an empty items array; hebcal-web applies the filter before its own "no events" check and answers 400.
  • /shabbat with b=0 recomputes the candle-lighting times after the calendar is built: hebcal-go's CheckCandleOptions rewrites a zero CandleLightingMins to the 18/20-minute default, so there is no way to ask it for sunset itself. Drop the workaround if hebcal-go grows one.

For the PDF calendars, where the bar is that a rendered calendar is indistinguishable from the one production serves for the same URL:

  • Chanukah candle-lighting order on Saturday night. On a motzei-Shabbat Chanukah day the same rows and times appear, but the "Chanukah: N Candles" candle-lighting row is grouped with the day's other timed rows at the foot of the cell here, where production places it at the top. Same content, an accepted ordering difference — not a missing, extra, or merged row.
  • Zmanim differ by up to about a minute, for the same reason as the JSON routes above.
  • Day numbers sit ~3.3pt lower than www.hebcal.com (and the month title ~6pt), a small, uniform vertical offset in the Latin faces; horizontal alignment is unaffected. The currently deployed download.hebcal.com places them exactly where this service does, so the offset shows up only against the newer of the two production builds.
  • A localized /v4/ alternate date reads 12 Tewet, not 12. Tewet. The day line comes from hebcal-go's hebrewDateEvent.Render(), which is behind @hebcal/hdate on the ordinal it writes for locales other than English and Spanish. The fix belongs in hebcal-go.
  • Two holidays on one day can be ordered differently. hebcal-go sorts a date's holidays alphabetically; @hebcal/core keeps the order it created them in. So a cell holding both Rosh Chodesh Elul and Rosh Hashana LaBehemot, or both Erev Purim and Shabbat Zachor, can list them the other way round — one or two cells a year, with no row missing, added or retimed.
  • An unknown-location 404 is not cached. hebcal-web lets its 14-day Cache-Control survive onto the 404; this service omits it there, since a location missing today may be added later. The out-of-range 410 and the rendered PDF are cached as in production.

Package layout

The service follows the standard Go microservice layout: cmd wires dependencies and starts the listener, internal holds the application, and pkg holds the two pieces that are useful on their own.

cmd/hebcal-api/          main: config, logger, data sources, HTTP listener
internal/
  config/                flag/env configuration and build metadata
  handler/               transport layer: one file per route, plus the mux
  service/               business logic, one package per route
    converter/             /converter parsing and JSON/XML/CSV rendering
    zmanim/                /zmanim halachic times and date resolution
    shabbat/               /shabbat calendar, candle options, item rendering
    complete/              /complete result serialization
    location/              query -> location, and the two location JSON shapes
    pdf/                   /v4/ and /v2/ PDF calendars: protobuf -> options,
                           event generation, page layout, shaping, fonts, links
    holidaypdf/            /holidays/hebcal-<year>.pdf URL parsing
  repository/
    readings/              client for the readings-svc sidecar (/leyning
                           for /shabbat, /learning for the PDF calendars)
                           over its unix domain socket
  model/                 domain layer: dates, locales, calendar events, errors
  httpx/                 shared transport utilities: ETag, CORS, content types,
                         error rendering, compression/metrics/logging middleware
  jsutil/                JavaScript-compatibility helpers (parseInt,
                         JSON.stringify, ordered objects, query conventions)
  logger/                pino-compatible JSON access log
pkg/
  geodb/                 SQLite geonames/zips reader and geographic typeahead;
                         a Go port of @hebcal/geo-sqlite
  geoip/                 client for the geoip2 unix-socket lookup service
  downloadpb/            the Download protobuf a /v4/ URL carries, shared
                         with hebcal-web
tools/                   porting tools: PDF comparison, width measurement,
                         locale and URL dumps (not part of the build)

pkg/geodb, pkg/geoip and pkg/downloadpb depend only on their own third-party libraries, never on internal, so either can be reused by another program or split out into its own module without untangling anything first. geodb embeds city2geonameid.json and carries the US state-name table, so it needs no data files beyond the two SQLite databases.

The dependency direction is one-way: handlerservicerepository, model, pkg. Nothing in service writes to an http.ResponseWriter — the handlers own status codes, headers, and cache validators, and the services return values.

Build and test

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 from ./cmd/hebcal-api (CGO_ENABLED=1)
make test      # runs the unit tests
make vet       # go vet
make fmt       # gofmt -w cmd internal pkg

If you invoke go directly rather than through the Makefile, add the tag yourself, e.g. go test -tags sqlite_fts5 ./....

The tests that render a calendar need the fonts: they look for $FONT_DIR, then a fonts directory at the repository root, and skip rather than fail when neither is present.

Run

./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.sqlite3 \
    -readings-socket /run/hebcal/readings-svc.sock \
    -fonts /var/www/fonts

The 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). /shabbat needs readings-svc listening on -readings-socket (or READINGS_SOCKET) for Torah readings, and the PDF calendars need -fonts (see Fonts).

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.

Deploy (Debian 13)

sudo make install       # installs binary, systemd unit, logrotate config
sudo systemctl start hebcal-api

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

Varnish decides which URLs reach this service. Alongside the JSON APIs, two PDF families now belong here rather than to the Node.js service: download.hebcal.com/v4/**.pdf (with its legacy /v2/h/**.pdf spelling) and www.hebcal.com/holidays/hebcal-*.pdf (and nothing else under /holidays/). Both used to be served by the separate hebcal-pdf-go process on port 8083, which this service replaces — route them to 8082 and retire that backend.

About

Hebrew Calendar HTTP APIs as microservice in Go

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages