Skip to content

Veterinary appointment watcher

Status: Current | Last reviewed: 2026-07-16

Use vet-appointment-watcher.mjs to poll the configured Vetstoria clinic, book an earlier real time slot, retain the new appointment, cancel only the superseded appointment, and optionally send a Twilio SMS.

Warning: This script calls an undocumented third-party API. Vetstoria can change the response or booking flow without notice. Run a dry check after any failure before returning it to live mode.

Configure the watcher

Copy vet-appointment-watcher.env.example to a private path ignored by Git. Fill in the owner and pet fields. Set VET_INITIAL_CUTOFF_AT to the latest appointment the first run may accept.

The script compares clinic-local YYYY-MM-DD HH:MM:SS strings. Once it confirms a booking, the persisted appointment becomes the next cutoff, so later runs only accept earlier slots.

Slots starting less than VET_MIN_LEAD_MINUTES from now (default 30, clinic-local via VET_CLINIC_TIMEZONE, default America/New_York) are never booked — imminent slots tend to be taken between selection and confirmation, which fails with HTTP 409 and trips the manual-review halt. The "no sooner appointment found" log line reports skippedTooSoon when such slots were passed over.

VET_DRY_RUN defaults to true. Twilio fields are optional; without all four values, the watcher logs the confirmed booking but does not send a text.

Test before booking

Run the unit tests and one live availability check with booking disabled:

node --test scripts/vet-appointment-watcher.test.mjs
node --env-file=/private/path/vet-watcher.env scripts/vet-appointment-watcher.mjs --once --dry-run

The availability parser ignores dates whose slot lists are empty. A log entry with dry run found a sooner appointment identifies the slot that live mode would use.

Run live

Run one live check first:

node --env-file=/private/path/vet-watcher.env scripts/vet-appointment-watcher.mjs --once --live

Then run the five-minute loop under a process supervisor:

node --env-file=/private/path/vet-watcher.env scripts/vet-appointment-watcher.mjs --live

Durable run in the dev devcontainer

The devcontainer has no cron or systemd (PID 1 is docker-init), so a bare node process dies when the container restarts. For a durable personal run:

  1. Put the private env at .local-data/vet-appointment-watcher/watcher.env (that tree is gitignored).
  2. .local-data/vet-appointment-watcher/supervise.sh runs the --live watcher and auto-restarts it on crash with capped exponential backoff. Launch it detached: setsid nohup .local-data/vet-appointment-watcher/supervise.sh >/dev/null 2>&1 & (writes supervise.pid; stop with kill "$(cat .local-data/vet-appointment-watcher/supervise.pid)").
  3. To also survive a container restart, .devcontainer/wake-up.sh (the postStartCommand) sources an optional untracked hook, .local-data/wake-up-local.sh, if present. That hook starts the supervisor idempotently on every boot. Both .local-data/ files are personal and untracked, so nothing appears in the shared diff.

The state defaults to .local-data/vet-appointment-watcher/state.json, uses mode 0600, and is ignored by Git. The watcher confirms the replacement first, atomically records it, and only then cancels the prior tracked booking. Failed cancellations remain queued for the next run.

Vetstoria can return a numeric ID and API hash before it exposes the public bkn... confirmation hash. The appointment remains tracked, but automatic cancellation requires the public hash. Copy the bkn... value from the confirmation link into currentAppointment.bookingHash in the private state file when that field is absent.

Only one process may use a state file. A lock prevents an overlapping cron or supervisor invocation from creating duplicate bookings.

Recover an uncertain confirmation

The script writes confirmationInFlight before sending the final confirmation. If the process loses the response, the next run stops live actions and requests manual review instead of risking a duplicate appointment.

Inspect the Vetstoria booking confirmation and the private state file. When you know the result, set currentAppointment to the confirmed booking and set confirmationInFlight to null, or clear only confirmationInFlight if no booking occurred. Preserve the numeric booking ID and bkn... hash so a future replacement can cancel it.

See also