Install
openclaw skills install @chrischall/pickuppatrol-mcpRead and change your children's school dismissal plans on PickUp Patrol (app.pickuppatrol.net) from a shell with curl — students, weekly defaults, day-by-day changes, school cutoff times.
openclaw skills install @chrischall/pickuppatrol-mcpapp.pickuppatrol.net is a ServiceStack JSON API behind an Ionic/Vue SPA. It is
reachable server-side with plain curl — no bot wall, no captcha, no
browser bridge. Use this when you want PickUp Patrol data in a script or on a
machine without the pickuppatrol-mcp server.
Full captured shapes: references/api.md.
export PUP_USER='you@example.com'
read -rs PUP_PASS && export PUP_PASS # never put the password in a command line
export PUP=https://app.pickuppatrol.net/api/json/reply
pup_login() {
local jar; jar=$(mktemp -t pupjar)
curl -sS -c "$jar" -X POST "$PUP/Authenticate" \
-H 'Content-Type: application/json' \
-d "$(jq -nc --arg u "$PUP_USER" --arg p "$PUP_PASS" \
'{provider:"credentials",UserName:$u,Password:$p,RememberMe:true}')" \
-o /tmp/pup_auth.json -w '%{http_code}' >/tmp/pup_code
[ "$(cat /tmp/pup_code)" = 200 ] || { jq -r '.ResponseStatus.Message' /tmp/pup_auth.json >&2; return 1; }
export PUP_JAR="$jar"
# This deployment returns cookies only — BearerToken comes back null. The
# JWT path exists server-side, so keep a token if one ever appears.
PUP_TOKEN=$(jq -r '.BearerToken // empty' /tmp/pup_auth.json); export PUP_TOKEN
}
# Every call goes through this: it sends the session cookies, and the bearer
# token as well when the deployment issued one.
pup() {
local method=$1 dto=$2; shift 2
local -a auth=(-b "$PUP_JAR")
[ -n "$PUP_TOKEN" ] && auth+=(-H "Authorization: Bearer $PUP_TOKEN")
curl -sS -X "$method" "$PUP/$dto" -H 'Content-Type: application/json' "${auth[@]}" "$@"
}
Never re-run a rejected login. PickUp Patrol answers bad credentials with
ResponseStatus.ErrorCode starting LOGIN-ERROR, counts the attempt against
the account, and a lockout clears only through their support desk. One failure
= stop and fix the credentials.
pup GET GetSession | jq '{name:.DisplayName, email:.Email, kids:[.Children[].StudentId]}'
pup GET GetChildren | jq -r '.[] | "\(.StudentId) \(.FirstName) — \(.SchoolName)"'
# Weekly default plan for one student. DayId is 1-based, SUNDAY = 1.
pup GET GetChildren | jq '.[] | select(.StudentId==1050046) | .DefaultPlans
| sort_by(.DayId) | map({day:.WeekDayName, option:.TransportationName, note:.Note})'
# The dismissal options a school offers, with the rules each imposes.
pup GET "GetTransportations?SchoolId=1703" | jq -r '.[] | select(.IsActive)
| "\(.TransportationId)\t\(.Name)\tnote=\(.IsNoteRequired)\tcar=\(.UseCarNumbers)\tearly=\(.IsEarlyDismissal)"'
# GetPlanEdit returns the date's OVERRIDE, not the effective plan: a date with
# no specific plan reads back null even when a weekday default exists.
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '{TransportationName,Note,IsLocked}'
pup GET "GetInvalidPlanDates?SchoolId=1703" | jq 'length' # non-school days
Writes change how a child actually leaves school. Read
GetTransportations first and obey the option's rules, or the request is
rejected (or worse, silently ignored):
IsNoteRequired → Note must be non-emptyUseCarNumbers → send CarNumber; otherwise omit the keyIsEarlyDismissal → send EarlyDismissalTime as HH:MM:SS; otherwise omit itIsLimited → only usable if the id is in the student's LimitedIdsChange specific dates — Plans is an array, one entry per date:
pup PUT UpdatePlans -d '{"Plans":[{
"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
"TransportationId":41246,"TransportationName":"PickUp","Note":"Chris Hall"}]}'
Clear a date back to the weekly default — a null transportation:
pup PUT UpdatePlans -d '{"Plans":[{
"StudentId":1050046,"SchoolId":1703,"PlanDate":"2026-08-17",
"TransportationId":null,"TransportationName":"Default plan","Note":null}]}'
Change the weekly defaults — there is no default-plans endpoint. Read the
whole student, edit DefaultPlans, PUT the whole record back
(references/api.md has the read-modify-write recipe).
A 2xx is not proof. The school silently ignores a change past its cutoff time.
pup GET "GetPlanEdit?PlanDate=2026-08-17&StudentId=1050046" | jq '.TransportationId'
Compare TransportationId and Note — never ModifiedDate, which advances
on its own and would make every write look successful. The note matters: every
option seen so far is IsNoteRequired, so a note-only edit is ordinary, and an
id-only comparison would pass without observing it. After clearing a date,
expect null — not the weekday default.