ParkWhiz Automated Delivery

Proposal, repo overhaul & live test results — July 7, 2026 · session 167cacfc
TL;DR — it works, and it's already been proven on a real order.
ParkWhiz's official v4 API supports pass transfers directly (POST /v4/bookings/{id}/share). A live test today delivered order #5726199193755 (Shinedown 8/14, Spokane) to the customer in ~3 seconds — no browser, no widget scraping. The API returns a share record (id, status, timestamps, recipient) that is machine-verifiable proof of transfer, and it 403s any second transfer attempt, so duplicate delivery is impossible. The parkwhiz-delivery repo has been overhauled accordingly (PR #2).

1 · What exists today

PieceCurrent state
Fulfillment portal (xhp-job-server fulfillment/) Human flow: agent claims order → buys pass on parkwhiz.com with a stored purchase account → marks completed. The "Delivery handoff" panel shows credentials for a manual transfer. No delivery automation, no proof storage, no delivered badge.
parkwhiz-delivery service (Railway) Standalone tool: operator pastes pass id + account creds + customer email; Playwright logs into parkwhiz.com, drives the transfer widget iframe by walking the DOM, screenshots the row, uploads to Automatiq. ~90+ s per delivery, fragile (cookie banners, obfuscated CSS classes, sleeps), creds typed by hand each time. Not integrated with the portal at all.
Proof of transfer Screenshot only (base64 in the service's SQLite). Nothing in the portal. Automatiq upload existed but the API token was hardcoded in git.
Today's test exposed a real process gap: the Shinedown order was marked completed in the portal at 14:00 UTC, but ParkWhiz showed the pass had never actually been transferred — the share attempt returned 201 (fresh transfer) instead of 403 (already shared). Automatiq agreed (transfer_count: 0, has_manual_proof: false). "Marked done" and "actually delivered" are different facts today; the API makes the second one checkable.

2 · The discovery: ParkWhiz has an official transfer API

The transfer widget the browser automation fights with is just a client of the public v4 API (developer.parkwhiz.com/v4, stable since 2022). Three calls replace the whole flow:

1
AuthenticatePOST /v4/oauth/token with grant_type=password, the purchase account's email/password. No partner client_id required. Tokens are valid ~1 year → cache per account, log in once.
2
ValidateGET /v4/bookings/{pass_id}. The pass number shown in the ParkWhiz UI (and recorded as purchase_order_number in the portal's completion metadata) is the booking id. Confirms the pass exists on this account, matches the event window, and isn't cancelled.
3
TransferPOST /v4/bookings/{pass_id}/share with {"email": customer_email}. HTTP 201 returns the booking-share model — the proof. The recipient needs no ParkWhiz account; they get an email with a claim link.

Built-in safety semantics

ResponseMeaning
201 + share modelTransfer performed. Proof: id, shared_status (pending → accepted when claimed), shared_at, shared_with[].
403 cannot_share_booking_already_sharedPass already transferred — duplicate delivery is structurally impossible.
409 booking_already_shared_with_userThis exact recipient already has it — safe idempotent outcome.
404 booking_not_foundWrong pass id / wrong account — caught before anything is sent.
DELETE /v4/bookings/{id}/share/{share_id}Revoke path for refunds/mistakes.

3 · Live test evidence (order #5726199193755)

Order
viagogo 648045314 · Sync 112526875 · listing 5726199193755
Event
PARKING PASSES ONLY Shinedown — Fri Aug 14 2026, Spokane Veterans Memorial Arena lots
Pass / booking
808317024 (purchased 2026-07-07 13:59:56Z on the s•••••40 purchase account, $21.88)
Customer
J. R. <j•••••••••••50@gmail.com> (full details in the session's proof artifact)
Auth
password grant → 200, token expires_in ≈ 1 year
Transfer
POST /share → 201 · share id 2907145 · status pending · 2026-07-07 15:39:21Z
Idempotency check
second POST /share → 403 cannot_share_booking_already_shared
Read-back
GET /bookings?q=shared_by_authenticated_user:true lists shared bookings ✓
Recorded
listing_activity_log id 13957 (parking_delivery_saved, delivery_automated: true, full proof JSON)

Raw proof JSON (share id, recipient, timestamps): delivered as a downloadable artifact in the chat session (proof-order-5726199193755.json), not published here.

4 · Proposed pipeline: auto-deliver every purchased order

Everything needed already lives in xhp-job-server: purchase accounts with Vault-encrypted passwords (parking_accounts), the booking id (purchase_order_number in completion metadata), the customer email (Automatiq Sync order), and an activity log the portal already reads.

completion / sweep                 xhp-job-server                      ParkWhiz v4 API
──────────────────                ────────────────────────────────    ─────────────────
listing marked          ───►      POST /api/parking-auto-deliver      1. token (cached)
'purchased' w/ booking#           · load listing + sync order         2. GET /bookings/{id}
                                  · account + Vault password          3. POST /bookings/{id}/share
retry sweep (15 min)    ───►      · idempotency: skip if a                 │
                                    delivery_automated activity           ▼
manual button in        ───►        exists OR ParkWhiz 403s          201 share model = proof
handoff panel                     · write parking_delivery_saved
                                    (proof JSON + proof-card PNG)
                                  · upload proof card to Automatiq
                                  · notify on failure (fulfillment_notifications)

Trigger policy

Why in the job server rather than the Railway service

5 · Portal UI: badge + proof button

All data flows through listing_activity_log action parking_delivery_saved — the record the handoff panel already reads — with new metadata keys (delivery_automated, parkwhiz_share_id, parkwhiz_share_status, proof, optional proof_card_data_url). No schema change.

ChangeWhere (exact anchor)
⚡ Auto-delivered badge on order rows/cards when the latest parking_delivery_saved has delivery_automated: true; tooltip shows share status (pending / accepted) fulfillment/public/fulfillment-app-02.jsparkingCardStatusBadge() (~:527) and renderListingTableRows() (~:222); badge styles in fulfillment.css ~:510
View / download proof button — opens the proof card PNG + share JSON, downloadable renderParkingDeliveryHandoff() (~:1528) — same pattern as the existing Delivery-URL/PDF Open/Download buttons (~:1551)
Backend: get_parking_delivery_handoff returns the automated-delivery metadata; new auto_deliver_parking action fulfillment/api/listing_sections/02_listing_sync_and_assignments.pyhandle_get_parking_delivery_handoff (~:283), action registry (~:709)
Share-status refresh: on handoff open, re-check pending → accepted via the API read-back and update the badge same handler; GET /v4/bookings?q=shared_by_authenticated_user:true or re-POST guard

6 · Efficiency comparison

Browser flow (v1)API flow (v2, shipped in PR #2)
Time per delivery~90–120 s~2–4 s
Human involvementPaste 5 fields incl. passwordZero (pipeline) / one click (manual)
Failure modesCookie banners, DOM drift, iframe timing, login wallsTyped HTTP errors with exact meanings
ProofScreenshot pixelsVerifiable share record + rendered proof card
Duplicate protectionBest-effort DOM text checkEnforced by ParkWhiz (403)
InfraChromium in Docker (~1 GB image)Plain HTTPS; browser kept only as fallback
Login frequencyEvery deliveryOnce per account per ~year (token cache)

7 · Rollout plan

  1. Done Repo overhaulparkwhiz-delivery PR #2: API engine, auto/browser fallback, token cache, structured proof, engine badges, Dockerfile fix. Merge + redeploy on Railway (set AUTOMATIQ_API_TOKEN env var).
  2. Job-server integrationPOST /api/parking-auto-deliver + completion hook + 15-min retry sweep, per §4. Backlog sweep will also surface any other "completed but never actually transferred" orders like today's.
  3. Portal UI — badge + proof button per §5.
  4. Monitor — Sentry on failures, fulfillment notification on delivery_problem, weekly count of auto vs manual deliveries.

8 · Risks & mitigations

RiskAssessment / mitigation
Password-grant volume triggers ParkWhiz defensesToken cache means ~one login per account per year; delivery volume is low (a few/day). The widget uses this same public API.
Share email lands in customer spamSame email ParkWhiz sends for manual widget transfers — no change vs today. Share status stays pending until claimed; surface it in the portal badge.
Wrong recipientCustomer email comes from the Automatiq Sync order (the same source agents copy from today); booking is validated against the order's event window before sharing; DELETE …/share/{id} revoke path exists.
Marked-completed ≠ delivered backlogThe retry sweep's ParkWhiz-side check (share exists?) turns this unknown into an auditable list.
Credentials hygieneVault-stored passwords stay server-side in the job-server design. The Automatiq API token that was hardcoded in the repo has been removed from code — rotate it since it lives in git history.

Prepared by Orq session 167cacfc · 2026-07-07 · Evidence: live API test on order 5726199193755, ParkWhiz v4 docs, xhp-job-server + parkwhiz-delivery source.