My Hot Lunchbox from the shell
ordernow.myhotlunchbox.com exposes a plain JSON API behind an OAuth2 password
grant. It is reachable server-side — no browser, no extension, no bridge. Two
curl calls get you data: one to sign in, one per read.
Sign in once per shell
Credentials come from the environment; never paste them into a command line (that puts them in shell history).
export MHLB_USER='you@example.com'
export MHLB_PASS='…' # e.g. read -rs MHLB_PASS
export MHLB=https://ordernow.myhotlunchbox.com
mhlb_login() {
local resp
resp=$(curl -sS -X POST "$MHLB/api/auth/login" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Accept: application/json' \
--data-urlencode 'grant_type=password' \
--data-urlencode "username=$MHLB_USER" \
--data-urlencode "password=$MHLB_PASS" \
--data-urlencode 'scope=openid offline_access email profile roles') || return 1
MHLB_TOKEN=$(printf '%s' "$resp" | jq -r '.access_token // empty')
if [ -z "$MHLB_TOKEN" ]; then
printf '%s' "$resp" | jq -r '.error_description // .error // "login failed"' >&2
return 1
fi
export MHLB_TOKEN
}
# Authenticated GET. usage: mhlb_get /parent/childrenInfo [curl args…]
mhlb_get() {
local endpoint=$1; shift # NOT `path`: zsh ties $path to $PATH
curl -sS "$MHLB/api$endpoint" -H "Authorization: Bearer $MHLB_TOKEN" -H 'Accept: application/json' "$@"
}
mhlb_login && mhlb_get /auth/userinfo | jq '{name, email, students_count, pending_orders_count, parent_credit_value}'
A failed sign-in must not be retried. The server is OpenIddict and counts
failed attempts; repeated failures can escalate to a CAPTCHA and remove
server-side sign-in for that account entirely. If invalid_grant comes back,
stop and check the credentials.
The token lasts about an hour. Re-run mhlb_login when a call starts returning
401.
The three reads that answer most questions
# Who the students are — the id feeds everything else
mhlb_get /parent/childrenInfo | jq '.[] | {id, firstName, schoolName, gradeTeacher, isInactive}'
# The lunch calendar for a date range (POST, despite being a read).
# The fields are `start`/`end`. Using `startDate`/`endDate` returns 200 with an
# EMPTY events array — a silent wrong answer, not an error.
curl -sS -X POST "$MHLB/api/calendar/studentSchoolData" \
-H "Authorization: Bearer $MHLB_TOKEN" -H 'Content-Type: application/json' \
-d '{"start":"2026-09-01","end":"2026-09-30"}' | jq '.events[] | {studentId, id, start, className}'
# What is in the cart but not yet paid for
mhlb_get '/event/shoppingCart' | jq .
references/endpoints.md has the rest — deliveries, transactions,
subscriptions, gift cards, per-day order detail, and the printable reports.
Ordering is read-modify-write
There is no "add item X" call. To place or change an order you fetch the model, edit it, and post it back whole:
mhlb_get '/event/createOrder?eventId=123&studentId=456' > order.json
# edit quantities in order.json
curl -sS -X POST "$MHLB/api/event/createOrder" \
-H "Authorization: Bearer $MHLB_TOKEN" -H 'Content-Type: application/json' \
-d @order.json
Anything missing from the payload is cleared, not preserved.
The write request bodies are unverified — their paths and verbs were read
out of the site's own compiled client, but no write has been exercised against a
live account. Inspect what you are about to send, and re-read the resource
afterwards to confirm it landed. A 200 is not proof.
/payment/checkout charges a real card. Do not call it speculatively.
Reading the errors
| Status | Meaning |
|---|---|
400 + invalid_grant |
wrong username/password — do not retry |
401 on an API call |
token expired; run mhlb_login again |
403 |
the endpoint belongs to the school-admin or vendor role, not a parent |
non-JSON 200 |
either a /parentReports/print* PDF (expected — see references) or the session lapsed into an HTML page |
500 on /parentReports/printOrders |
usually a caller mistake: empty studentIds, or no order matching that date and status |
Prefer the MCP when it is available
myhotlunchbox-mcp wraps all of this with typed tools and a confirmation step
for every mutation — a prompt where the client can show one (unless the server
sets MCP_CONFIRM_ELICITATION=off), otherwise a preview plus a single-use
confirmToken.
Use this skill when the MCP is not installed, or inside a script.
