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).
| Piece | Current 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. |
transfer_count: 0, has_manual_proof: false). "Marked done"
and "actually delivered" are different facts today; the API makes the second one checkable.
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:
POST /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.GET /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.POST /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.| Response | Meaning |
|---|---|
201 + share model | Transfer performed. Proof: id, shared_status (pending → accepted when claimed), shared_at, shared_with[]. |
403 cannot_share_booking_already_shared | Pass already transferred — duplicate delivery is structurally impossible. |
409 booking_already_shared_with_user | This exact recipient already has it — safe idempotent outcome. |
404 booking_not_found | Wrong pass id / wrong account — caught before anything is sent. |
DELETE /v4/bookings/{id}/share/{share_id} | Revoke path for refunds/mistakes. |
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.
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)
status='purchased' parking listings with a booking # but no successful delivery record (covers API blips and the historical backlog).api_engine.py in PR #2 for the reference implementation).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.
| Change | Where (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.js — parkingCardStatusBadge() (~: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.py — handle_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 |
| Browser flow (v1) | API flow (v2, shipped in PR #2) | |
|---|---|---|
| Time per delivery | ~90–120 s | ~2–4 s |
| Human involvement | Paste 5 fields incl. password | Zero (pipeline) / one click (manual) |
| Failure modes | Cookie banners, DOM drift, iframe timing, login walls | Typed HTTP errors with exact meanings |
| Proof | Screenshot pixels | Verifiable share record + rendered proof card |
| Duplicate protection | Best-effort DOM text check | Enforced by ParkWhiz (403) |
| Infra | Chromium in Docker (~1 GB image) | Plain HTTPS; browser kept only as fallback |
| Login frequency | Every delivery | Once per account per ~year (token cache) |
AUTOMATIQ_API_TOKEN env var).POST /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.delivery_problem, weekly count of auto vs manual deliveries.| Risk | Assessment / mitigation |
|---|---|
| Password-grant volume triggers ParkWhiz defenses | Token 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 spam | Same email ParkWhiz sends for manual widget transfers — no change vs today. Share status stays pending until claimed; surface it in the portal badge. |
| Wrong recipient | Customer 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 backlog | The retry sweep's ParkWhiz-side check (share exists?) turns this unknown into an auditable list. |
| Credentials hygiene | Vault-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.