Hands-free class booking for Arbox gyms β book Β· standby Β· confirm Β· notify
A small TypeScript bot that books your preferred Arbox classes the moment registration opens, joins the standby list when a class is full, and auto-confirms standby slots before they expire β emailing you at every step. Runs as a long-lived scheduler with two cron jobs and a tiny HTTP server for one-click cancellations.
β οΈ Unofficial β uses a reverse-engineered Arbox API. This talks to the private Arbox v2 API (apiappv2.arboxapp.com) with your own credentials. It is a personal-use automation, not an Arbox product; the API can change without notice. Use at your own risk.
- Explore β What it is Β· How it works Β· Quick start Β· Architecture Β· Tech stack Β· Configuration Β· Testing Β· Deployment Β· Docs & decisions
Arbox opens bookings for the following week every Friday evening. arbox-schedule automates that
race so you don't have to sit at your phone:
- Friday 21:00 (Israel time) β the booking job fetches next week's schedule, finds your preferred classes by series ID (in priority order), and books up to 2 lessons. If a class is already full, it joins the standby list and persists the entry to local state.
- Every 5 minutes β the standby job polls open standby entries. When Arbox frees a slot (by
setting an
availability_id), the bot immediately confirms the booking and emails you. Expired entries are cleaned up. - Email at every step via Resend β a summary after each booking run, and individual alerts when a standby spot is confirmed, lost, or expires.
- One-click cancel links β booking emails include an HMAC-signed
/cancelURL so you can drop a reserved class straight from your inbox.
Targets next week (SundayβSaturday) and walks your series IDs in priority order β primary list first, then secondary β booking up to 2 lessons per week. For each candidate:
| Class state | Action |
|---|---|
| Already booked / on standby | Counts as a slot, skips |
Spot available (free > 0) |
Books immediately |
Class full (free == 0) |
Joins standby, records the entry in state.json |
Note on
has_spots: the Arbox API exposes ahas_spotsfield, but it reflects membership eligibility, not raw availability. The bot usesfree > 0instead. Seedocs/api.md.
When a booked user cancels, Arbox promotes the first person on standby by setting availability_id
on the schedule item and sending a notification β the user then has ~30 minutes to confirm. The
standby job polls every 5 minutes; when it sees a non-null availability_id for a tracked entry it
calls scheduleUser/insert with that ID to confirm. If the ID has already expired, the error is
logged and the entry is retried next cycle.
The scheduler also runs a tiny HTTP server (default port 3000) exposing:
| Endpoint | Purpose |
|---|---|
GET /cancel?token=β¦ |
Cancels a booking from a signed link embedded in booking emails (HMAC-SHA256, ~8-day TTL) |
POST /standby/run |
Fires the standby job on demand (202 started, or 409 already-running) |
Cancellation links are only generated when CANCEL_SECRET and BASE_URL are set.
Requires Node 22+ (or Docker). Install, configure, then run the scheduler:
npm install
cp .env.example .env # then fill in the values below
npm start # ts-node schedule/scheduler.ts β runs until killedOn start it logs the registered jobs and endpoints:
Booking job: every Friday at 21:00 Israel time (0 21 * * 5)
Standby job: every 5 minutes (*/5 * * * *)
Endpoints: POST /standby/run, GET /cancel?token=...
HTTP server: listening on port 3000
Discover your IDs β if you don't know your BOX_ID, LOCATION_ID, or MEMBERSHIP_ID, set just
your credentials and run the helper:
npx ts-node scripts/discover-ids.ts # prints all threeFind series IDs β each recurring class belongs to a stable series (e.g. "HIIT, Sunday, 08:10").
Dump next week's schedule with each class's series_fk:
npx ts-node scripts/debug-schedule.tsTrigger a booking run immediately (useful for testing):
npx ts-node scripts/trigger-booking.tsA single long-lived Node process registers two cron jobs and an HTTP server. All state is a single JSON file; the only outbound dependencies are the Arbox API and Resend.
flowchart LR
Cron["node-cron<br/>Fri 21:00 Β· every 5 min"]
HTTP["HTTP server :3000<br/>/cancel Β· /standby/run"]
Sched["scheduler.ts"]
Arbox["Arbox API v2"]
Resend["Resend (email)"]
State[("state.json")]
Cron --> Sched
HTTP --> Sched
Sched -- "book / standby / cancel" --> Arbox
Sched -- "notify" --> Resend
Sched -- "persist standby" --> State
schedule/
scheduler.ts # entry point β registers both cron jobs + HTTP server
booking.ts # Friday booking logic
standby.ts # standby polling and auto-confirmation
cancel.ts # HTTP server: signed /cancel links + /standby/run
state.ts # JSON-based state persistence (standby list)
config.ts # loads and validates env vars
notify.ts # email notifications via Resend
utils.ts # date helpers
api/
client.ts # Arbox HTTP client (reverse-engineered v2 API)
requests/ # auth Β· user Β· boxes Β· schedule calls
types/ # response shapes
scripts/
discover-ids.ts # one-off: prints BOX_ID, LOCATION_ID, MEMBERSHIP_ID
trigger-booking.ts # manually trigger the booking job
debug-schedule.ts # dump next-week schedule + test a booking attempt
docs/
api.md # reverse-engineered Arbox API v2 reference
State is written to state.json in the working directory (overridable via STATE_FILE). It tracks
which classes you are on standby for β the only persistent runtime artifact.
| Layer | Tech |
|---|---|
| Runtime | Node 22, TypeScript 5 (run via ts-node) |
| Scheduling | node-cron (Asia/Jerusalem timezone) |
| HTTP server | Node http (no framework) |
| Resend | |
| Config | dotenv |
| Tests | Vitest |
| Lint / format | ESLint + Prettier (Husky pre-commit) |
| Packaging | Docker (node:22-alpine) |
Copy .env.example to .env and fill in the values:
| Variable | Description |
|---|---|
ARBOX_EMAIL |
Your Arbox login email |
ARBOX_PASSWORD |
Your Arbox password |
BOX_ID |
Numeric ID of your gym (box) |
LOCATION_ID |
Numeric ID of the gym location (locations_box ID) |
MEMBERSHIP_ID |
Your active membership record ID (required to book) |
PRIMARY_SERIES_IDS |
Comma-separated series IDs β booked first, in order (e.g. 76644881,76647404) |
SECONDARY_SERIES_IDS |
Comma-separated fallback series IDs β used if primary slots are full |
RESEND_API_KEY |
API key from resend.com |
NOTIFICATION_EMAIL |
Address that receives booking notifications |
CANCEL_SECRET |
(optional) HMAC secret for signed /cancel links |
BASE_URL |
(optional) Public base URL used to build /cancel links |
PORT |
(optional) HTTP server port (default 3000) |
STATE_FILE |
(optional) Path to the state JSON file (default ./state.json) |
During development, Resend sends from onboarding@resend.dev, which only delivers to your
Resend-verified address. Once you have a verified sending domain, update the from field in
schedule/notify.ts.
Email notifications:
| Event | Subject |
|---|---|
| Friday booking run complete | Arbox booking β N of 2 lessons scheduled |
| Standby spot confirmed | β
Standby confirmed |
| Standby position lost | β Standby slot lost |
| Standby entry expired (past) | βΉοΈ Standby expired |
npm run typecheck # tsc --noEmit
npm run lint # ESLint
npm run format # Prettier (write)
npm test # Vitest unit testsTests live in tests/ and cover booking, standby, cancellation, and the HTTP server
against mocked Arbox API responses.
The repo ships a Dockerfile ready for Coolify or any Docker-compatible host:
docker build -t arbox-schedule .
docker run -d \
--env-file .env \
-v $(pwd)/state.json:/app/state.json \
arbox-scheduleThe container runs ts-node schedule/scheduler.ts on startup. Mount a persistent volume at the
STATE_FILE path (default /app/state.json) so standby state survives restarts. On Coolify: push the
repo, let it detect the Dockerfile, set the env vars, and mount the state volume.
- Arbox API v2 reference:
docs/api.mdβ auth flow, request/response shapes, error codes, and non-obvious behavior (thehas_spotsvsfreedistinction, the standby confirmation flow). - Design spec:
docs/superpowers/specs/2026-04-25-lesson-scheduler-design.md - Implementation plan:
docs/superpowers/plans/2026-04-25-lesson-scheduler.md
No license file is currently present in this repository β all rights reserved by default. Add a
LICENSE to clarify reuse terms.