docs: add ParkSmarter API usage guide + endpoint reference
- docs/PARKSMARTER_API.md: narrative guides (bootstrap/auth, finding parking, estimates, initiating a transaction, sessions/receipts, account) + per-endpoint reference + a behaviors/gotchas table (200-on-error, stale-token empty body, multi-estimate fallback, free windows, flat-rate zones, no free check-in). - client README: link the guide; promote PastSession/ParkingReceipt to CONFIRMED (ActiveSession still pending — no active session at capture time). - Main README: mark "Start a paid session" as working (confirmed live end-to-end). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8a2a87a1a9
commit
70047b3964
3 changed files with 346 additions and 13 deletions
|
|
@ -38,8 +38,8 @@ bigbrainparking/
|
|||
| "Near my last location" proximity search | ✅ wired | caches last GPS fix |
|
||||
| QR kiosk scan → meter lookup | ✅ wired | on-device VisionCamera |
|
||||
| Save / share kiosks | ✅ wired | local (no server favorites API exists) |
|
||||
| Active / past sessions | ✅ wired | list views |
|
||||
| Start a paid session | 🟡 gated | flow wired to `postStartParkingSession`, disabled pending review (real charge) |
|
||||
| Active / past sessions | ✅ wired | list views + tap for full receipt |
|
||||
| Start a paid session | ✅ works | confirmed live end-to-end (real $0.10 DL-zone charge); declines surfaced |
|
||||
| Session-expiry reminders | ✅ wired | **local** on-device notifications — no server, no push |
|
||||
| UnifiedPush (ntfy) | ⚪ optional | not needed for reminders; stub for future server-initiated msgs |
|
||||
|
||||
|
|
|
|||
321
docs/PARKSMARTER_API.md
Normal file
321
docs/PARKSMARTER_API.md
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
# ParkSmarter API — usage guide & endpoint reference
|
||||
|
||||
A commented guide to the ParkSmarter (IPS Group) API as exposed by
|
||||
[`parksmarter-client`](../parksmarter-client). It reverse-engineers the official
|
||||
Android app (`com.ipsgroupinc.parksmarter` 4.4.0) and has been verified against
|
||||
production. For install/quick-start and the auth-header model, see the
|
||||
[client README](../parksmarter-client/README.md); this document focuses on **how to
|
||||
actually use the API** — searching for parking and running a transaction — plus a
|
||||
per-endpoint reference and the behavioral quirks worth knowing.
|
||||
|
||||
> Everything here is for interoperability/research with **your own** account. All
|
||||
> methods below are on a `ParkSmarterClient` instance (`const ps = new ParkSmarterClient(...)`).
|
||||
|
||||
---
|
||||
|
||||
## Mental model
|
||||
|
||||
- **Zone** — a metered area (e.g. Sandpoint `DSB3`, `DL`). Has an id (`ZoneId`), a
|
||||
`CustomerId` (the operator/city, e.g. `217` = "Sandpoint, ID"), and one or more **Spaces**.
|
||||
- **Space** — an individual stall/segment within a zone (`SpaceId`). Estimates and
|
||||
sessions are always for a specific `(zone, space)`.
|
||||
- **Policy** — a zone/space's schedule: a list of time slots, each with a `RateType`
|
||||
(`Free`, `Hour`, `Variable`, `No Parking`, `Prepay`), a `Rate`, a `MaxTime`, and a
|
||||
`CurrentSlot` flag marking the one in effect now. Policies are **descriptive** — they
|
||||
tell you when/what it costs; there is no "check-in", grace-token, or free-session
|
||||
concept in the API.
|
||||
- **Estimate** — a price quote for parking a given `(zone, space, vehicle)` for some
|
||||
duration. Three flavors (multi / single / items) — see below.
|
||||
- **Session** — a paid parking transaction (`POST /api/Session`). Requires a card and a
|
||||
charge; there is **no $0 / free session**. Free parking is simply unmetered time.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bootstrap & authentication
|
||||
|
||||
Auth is **custom-header**, not OAuth. Call `getApplicationValidity()` first (seeds a
|
||||
`ParkSmarter_SessionId`), then log in. The client stores `Auth_Token` + `SessionId`
|
||||
automatically and refreshes the rolling `Auth_Token` from the `Response` envelope.
|
||||
|
||||
```ts
|
||||
const ps = new ParkSmarterClient({ environment: 'prodv2' });
|
||||
|
||||
const validity = await ps.getApplicationValidity();
|
||||
if (validity.Config?.IsInMaintenanceMode) throw new Error('maintenance');
|
||||
|
||||
await ps.loginWithPhone({ phoneNumber: '5551234567', password: '…' });
|
||||
// From here, authenticated endpoints work. Auth_Token is sent as a header.
|
||||
```
|
||||
|
||||
**Gotchas**
|
||||
- A **failed login returns HTTP 200** with `Status: "Error"` + null `Auth_Token`. The
|
||||
client throws `LoginError` for you; if you call `/api/Auth` yourself, check both.
|
||||
- A `POST /api/Auth` returning **201** means the account needs SMS verification — not a
|
||||
normal login. Treat it distinctly (`requestVerifyUser` → `verifyUser`).
|
||||
- **Validating a stored token:** a stale/expired `Auth_Token` does **not** 401 —
|
||||
`GET /api/User` just returns an **empty body** (`getUserDetail()` resolves to
|
||||
`undefined`). To check "am I really signed in?", require real fields:
|
||||
`const u = await ps.getUserDetail(); const ok = !!(u && (u.PersonalPhone || u.PersonalEmailAddress));`
|
||||
- Persist tokens across launches with a `TokenStore` (see README's RN example) so you
|
||||
don't have to log in every time.
|
||||
|
||||
---
|
||||
|
||||
## 2. Finding parking (search)
|
||||
|
||||
There are several ways in, all returning the same `MetersResponse` shape
|
||||
(`{ Zones: Zone[], Response }`). **Meter search requires a valid `Auth_Token`** — these
|
||||
lookups 401 when signed out, despite being "data" reads.
|
||||
|
||||
| You have… | Use | Sends |
|
||||
| --- | --- | --- |
|
||||
| A map location | `getMetersByLocation({ latitude, longitude })` | `Lat`,`Long` |
|
||||
| A map location (lighter list) | `getLimitedMetersByLocation({ latitude, longitude })` | `Lat`,`Long` |
|
||||
| A zone name (e.g. `"DL"`) | `getMetersByZoneName('DL')` | `ZoneName` |
|
||||
| Free-text zone/space | `searchMetersByZoneOrSpace('Main St')` | `Query` |
|
||||
| A meter serial number | `getMetersBySerialNumber('…')` | `TerminalSerNo` |
|
||||
| A scanned QR/barcode | `getMetersByScannerCode('DSB13')` | `ScannerCode` |
|
||||
| Gated lots w/ occupancy | `getParkingLots()` | — |
|
||||
|
||||
```ts
|
||||
// By location (only a single coordinate is ever sent — the point you're searching):
|
||||
const res = await ps.getMetersByLocation({ latitude: 48.2754, longitude: -116.5478 });
|
||||
const zones = res.Zones ?? [];
|
||||
|
||||
// Pick a zone + space to act on:
|
||||
const zone = zones[0];
|
||||
const space = zone.Spaces?.[0];
|
||||
const base = {
|
||||
zoneId: zone.ZoneId!,
|
||||
spaceId: space!.SpaceId!,
|
||||
customerId: zone.CustomerId!,
|
||||
};
|
||||
|
||||
// Is it free / paid right now? Read the CurrentSlot policy:
|
||||
const current = space!.Policies?.find((p) => p.CurrentSlot);
|
||||
const free = (current?.RateType ?? '').toLowerCase().includes('free');
|
||||
```
|
||||
|
||||
**Notes**
|
||||
- `Zone.Lat`/`Long` in results is the **meter's** location (for map pins), not yours.
|
||||
- QR codes on kiosks are often a generic `parksmarter.com/home/processQR` URL with **no
|
||||
zone in it** — fall back to prompting for the printed Zone ID and
|
||||
`getMetersByScannerCode`/`getMetersByZoneName`.
|
||||
- Scanner codes map to zones: e.g. `DSB13` → zone `113150`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Getting a price (estimates)
|
||||
|
||||
Before a transaction you quote a price for a `(zone, space, vehicle)`. Three endpoints:
|
||||
|
||||
| Method | Path | Use |
|
||||
| --- | --- | --- |
|
||||
| `getParkingEstimateMulti(base)` | `/api/ParkingEstimateMulti` | A **ladder** of durations → prices (the app's default). |
|
||||
| `getParkingEstimateSingle({…, durationInMinutes, creditCardId})` | `/api/ParkingEstimate` | One duration → one price. |
|
||||
| `getParkingEstimateItems(base)` | `/api/ParkingEstimateItems` | Item/product-style options. |
|
||||
|
||||
```ts
|
||||
const vehicleId = (await ps.getUserDetail()).VehicleDetails![0].VehicleID!;
|
||||
const q = { ...base, vehicleId };
|
||||
|
||||
const multi = await ps.getParkingEstimateMulti(q);
|
||||
const ladder = multi.ParkingDetail ?? []; // [{ Minutes, StartTime, EndTime, ParkingCost, TransactionFee }, …]
|
||||
```
|
||||
|
||||
**This is where most of the quirks live:**
|
||||
|
||||
- **Multi is not universal.** Some zones (e.g. short-term `DL` spaces) reject
|
||||
`ParkingEstimateMulti` with HTTP 200 + `Response.Status: "Error"`
|
||||
("Unable to process your request…") even though **single works**. Detect the error
|
||||
envelope and fall back to `getParkingEstimateSingle` across the zone's `MinTime..MaxTime`.
|
||||
- **Free windows signal via `MaxTime: 0` / `$0.00`.** During a free period the single
|
||||
estimate returns `MinTime: 5, MaxTime: 0, ParkingCost: "0.00"` and multi returns an
|
||||
empty/error ladder. Treat `MaxTime <= 0` (helper: `isFreeEstimate`) or an all-`$0.00`
|
||||
ladder (`ladderAllFree`) as "currently free — just park", **not** a purchasable option.
|
||||
- **Flat-rate zones.** Some zones charge one flat price for any duration up to a cap
|
||||
(e.g. `DL` = **$0.10 flat** until the ~5 PM boundary). Every single-estimate duration
|
||||
returns the same `ParkingCost`; `MaxTime` is *minutes remaining until the paid-window
|
||||
boundary* and shrinks through the day. Offer one option ("park to MaxTime"), not a
|
||||
ladder of identical prices.
|
||||
- **`ParkingDetail.Minutes` may be `0`** on single estimates — the real duration is in
|
||||
`StartTime`/`EndTime`. Trust the duration you requested.
|
||||
- **Estimates need auth** (like meter search).
|
||||
|
||||
---
|
||||
|
||||
## 4. Initiating a transaction (start a session)
|
||||
|
||||
`startParkingSession()` (`POST /api/Session`) is the **only** session-creating call, and
|
||||
it **charges the card**. Build it from a chosen estimate rung:
|
||||
|
||||
```ts
|
||||
const rung = ladder[selectedIndex]; // from a multi/single estimate
|
||||
const res = await ps.startParkingSession({
|
||||
creditCardId: card.CCID!,
|
||||
vehicleId,
|
||||
zoneId: base.zoneId,
|
||||
spaceId: base.spaceId,
|
||||
customerId: base.customerId,
|
||||
meterTypeId: zone.MeterTypeId!,
|
||||
startTime: rung.StartTime!,
|
||||
endTime: rung.EndTime!,
|
||||
minutesToPurchase: rung.Minutes!,
|
||||
parkingCost: Number(rung.ParkingCost ?? 0),
|
||||
transactionFee: Number(rung.TransactionFee ?? 0),
|
||||
minCreditAmount: zone.MinimumAmount, // optional
|
||||
});
|
||||
```
|
||||
|
||||
**Request body** carries `CCID, Amount (cost+fee), SpaceID, StartTime, EndTime,
|
||||
CustomerID, VehicleID, TimePurchased, ParkingCost, TransactionFee, ZoneID,
|
||||
MinCreditAmount?, MeterTypeId` (and `BleEncBytes` for BLE meters). **No device location
|
||||
is sent.**
|
||||
|
||||
**Gotchas**
|
||||
- **A decline still returns HTTP 200**, with `Response.Status: "Error"` and often
|
||||
`OriginalErrorMessage: "DECLINED"`. The client throws on this envelope so you don't get a
|
||||
phantom success — surface `serverMessage` to the user.
|
||||
- **You cannot buy during a free window.** The app blocks purchase
|
||||
("Purchases are currently not allowed. Parking is currently free.") and the API rejects
|
||||
zero-minute / zero-cost sessions (`unableToPurchaseZeroMinutes`, `minTimeNotReached`).
|
||||
- **No free check-in.** There is no way to register a $0 session; free time is just
|
||||
unmetered — you park, enforcement (LPR/chalk) handles the limit.
|
||||
- Schedule your own **local** expiry reminder from `EndTime`; the API has no per-session
|
||||
reminder push.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sessions & receipts
|
||||
|
||||
```ts
|
||||
const active = (await ps.getActiveParkingSessions()).ParkingSession ?? [];
|
||||
const past = (await ps.getPastParkingSessions({ currentPage: 1, pageSize: 20 })).Session ?? [];
|
||||
|
||||
// Full receipt for a past session (auth code, payment, amounts):
|
||||
const tid = past[0]?.TransactionID;
|
||||
const receipt = (await ps.getParkingReceipt(tid!)).ParkingReceipt;
|
||||
await ps.emailParkingReceipt(tid!); // emails it to the account holder
|
||||
```
|
||||
|
||||
- `PastSession` uses `Zone`/`Description` (not `ZoneName`) for the zone label, and echoes
|
||||
`Lat`/`Long` of the meter.
|
||||
- `ParkingReceipt` has `PaymentType`, `PaymentDisplay` ("MASTERCARD"), masked `CC`,
|
||||
`AuthCode`, `Vehicle` (plate), `Amount`/`TransactionFee`/`Total` (display strings) plus
|
||||
`*Value` numerics.
|
||||
|
||||
---
|
||||
|
||||
## 6. Account management
|
||||
|
||||
| Task | Method(s) |
|
||||
| --- | --- |
|
||||
| Vehicles | `addVehicle`, `updateVehicle`, `deleteVehicle` (list via `getUserDetail().VehicleDetails`) |
|
||||
| Cards | `addCard`, `updateCard`, `setCardDefault`, `deleteCard` (list via `getUserDetail().CreditCardDetails`) |
|
||||
| Profile | `updateProfile`, `requestDeleteUser` |
|
||||
| Password | `requestResetPassword`, `updatePassword` |
|
||||
| Notifications | `getNotificationSettings`, `setNotificationSettings` |
|
||||
|
||||
Mutations return a `{ Status, Message }` envelope — treat `Status === 'Success'` as the
|
||||
success signal. Card numbers are only ever **sent** (add/update); responses mask them.
|
||||
|
||||
---
|
||||
|
||||
## Behaviors & gotchas (quick reference)
|
||||
|
||||
| Behavior | Detail |
|
||||
| --- | --- |
|
||||
| 200-on-error | Failed login / declined payment / bad request often return **HTTP 200** with `Response.Status: "Error"`. Always check the envelope, not just the status code. |
|
||||
| Stale token ≠ 401 | An expired `Auth_Token` yields an **empty** `/api/User` body, not a 401. Validate by requiring real user fields. |
|
||||
| Rolling token refresh | Non-empty `Response.Auth_Token` (or top-level) on any response replaces your token — the client stores it. |
|
||||
| Meter/estimate search needs auth | `/api/Meter`, `/api/MeterList`, and estimates 401 when signed out. |
|
||||
| Multi estimate unsupported on some zones | Falls back to single; detect `Response.Status: "Error"`. |
|
||||
| Free window = `MaxTime 0` / `$0.00` | Show "currently free", don't offer a $0 ladder. |
|
||||
| Flat-rate zones | One price for any duration; `MaxTime` = minutes to the paid-window boundary. |
|
||||
| No free check-in | Only paid sessions exist; free parking is unmetered time. |
|
||||
| `localeCode` | Every request carries `localeCode` (default `en`). |
|
||||
| Non-prod is IP-restricted | `dev`/`stage`/`test` return 403 "Web App - Unavailable" from the public internet. |
|
||||
|
||||
---
|
||||
|
||||
## Endpoint reference
|
||||
|
||||
All methods are on `ParkSmarterClient`. Friendly camelCase inputs are mapped to the wire
|
||||
format; responses are the raw server PascalCase JSON (typed in
|
||||
[`src/types.ts`](../parksmarter-client/src/types.ts)).
|
||||
|
||||
### Bootstrap
|
||||
- **`getApplicationValidity()`** → `GET /api/ApplicationValidity` — feature flags,
|
||||
maintenance/upgrade, seeds `SessionId`. Public. Call first.
|
||||
|
||||
### Auth
|
||||
- **`loginWithPhone({ phoneNumber, password })`** → `POST /api/Auth` — persists token.
|
||||
- **`loginWithApple({ emailAddress, appleId, providerAuth, providerIdentity })`** → `POST /api/Auth`.
|
||||
- **`loginWithCachedToken(authToken)`** — set a stored token (then bootstrap).
|
||||
- **`logoutAllDevices()`** → `POST /api/Auth/Logout` — server-side invalidate.
|
||||
- **`logoutLocal()`** — clear local tokens only.
|
||||
|
||||
### Sign-up / password / verification
|
||||
- **`signUp({ emailAddress, mobilePhone, password })`** → `POST /api/User`. Public.
|
||||
- **`requestResetPassword({ reqType, phoneNumber })`** → `POST /api/Password`. Public.
|
||||
- **`updatePassword({ oldPassword, newPassword })`** → `PUT /api/Password`.
|
||||
- **`requestVerifyUser({ phoneNumber })`** → `POST /api/UserVerification` — SMS code.
|
||||
- **`verifyUser({ phoneNumber, code })`** → `GET /api/UserVerification`.
|
||||
- **`isEmailRegistered(email)` / `isPhoneRegistered(phone)`** → `GET /api/User`. Public boolean.
|
||||
|
||||
### Profile / device
|
||||
- **`getUserDetail()`** → `GET /api/User` — profile incl. `VehicleDetails` + `CreditCardDetails`. (Empty body ⇒ token stale.)
|
||||
- **`updateProfile({ emailAddress, phoneNumber })`** → `PUT /api/User`.
|
||||
- **`requestDeleteUser()`** → `DELETE /api/User`.
|
||||
- **`updateDeviceToken({ pushNotificationsToken, imeiNumber, deviceType?, language })`** → `PUT /api/Device`. (BigBrainParking does **not** call this — avoids sending device id/IMEI.)
|
||||
|
||||
### Vehicles
|
||||
- **`addVehicle({ plate, state, vehicleAlias, isDefaultVehicle? })`** → `POST /api/Vehicle`.
|
||||
- **`updateVehicle({ id, plate, state, vehicleAlias, isDefaultVehicle? })`** → `PUT /api/Vehicle`.
|
||||
- **`deleteVehicle(vehicleId)`** → `DELETE /api/Vehicle`.
|
||||
|
||||
### Cards
|
||||
- **`addCard({ cardNumber, alias, expDate, zipCode, isDefaultCard? })`** → `POST /api/Card`.
|
||||
- **`updateCard({ id, … })`** → `POST /api/Card` (full replace).
|
||||
- **`setCardDefault({ id, isDefaultCard })`** → `PUT /api/Card`.
|
||||
- **`deleteCard(cardId)`** → `DELETE /api/Card`.
|
||||
|
||||
### Meters / zones
|
||||
- **`getMetersByLocation({ latitude, longitude })`** → `GET /api/Meter?Lat=&Long=`.
|
||||
- **`getLimitedMetersByLocation({ latitude, longitude })`** → `GET /api/MeterList?Lat=&Long=`.
|
||||
- **`getMetersByZoneName(zoneName)`** → `GET /api/Meter?ZoneName=`.
|
||||
- **`searchMetersByZoneOrSpace(query)`** → `GET /api/Meter?Query=`.
|
||||
- **`getMetersBySerialNumber(serial)`** → `GET /api/Meter?TerminalSerNo=`.
|
||||
- **`getMetersByScannerCode(code)`** → `GET /api/Meter?ScannerCode=`.
|
||||
- **`getParkingLots()`** → `GET /api/ParkingLogix` — gated lots + occupancy.
|
||||
|
||||
### Estimates
|
||||
- **`getParkingEstimateMulti({ zoneId, spaceId, customerId, vehicleId, minCreditAmount?, bleEncBytes? })`** → `GET /api/ParkingEstimateMulti` — duration ladder.
|
||||
- **`getParkingEstimateSingle({ …, durationInMinutes, creditCardId })`** → `GET /api/ParkingEstimate` — one duration.
|
||||
- **`getParkingEstimateItems(base)`** → `GET /api/ParkingEstimateItems`.
|
||||
- Helpers: **`isFreeEstimate(probe)`**, **`ladderAllFree(rungs)`** (pure; exported).
|
||||
|
||||
### Sessions / receipts
|
||||
- **`startParkingSession({ creditCardId, vehicleId, zoneId, spaceId, customerId, meterTypeId, startTime, endTime, minutesToPurchase, parkingCost, transactionFee, minCreditAmount?, bleEncBytes? })`** → `POST /api/Session`. **Charges the card.** Throws on the `Status:Error` decline envelope.
|
||||
- **`getActiveParkingSessions()`** → `GET /api/ParkingSession`.
|
||||
- **`getPastParkingSessions({ currentPage, pageSize })`** → `GET /api/Session`.
|
||||
- **`getParkingReceipt(transactionId)`** → `GET /api/ParkingReceipt`.
|
||||
- **`emailParkingReceipt(id)`** → `POST /api/ParkingReceipt` — email to account holder.
|
||||
|
||||
### Settings / content
|
||||
- **`getNotificationSettings()` / `setNotificationSettings(...)`** → `/api/Setting`.
|
||||
- **`getStates()`** → `GET /api/State`. (Note: seen returning 404 on prodv2 — pass an id form if needed.)
|
||||
- **`getAbout()` / `getFAQ()` / `getPrivacyPolicy()` / `getTerms()`** → `GET /api/ParkSmarter*` — static content.
|
||||
|
||||
---
|
||||
|
||||
## Verification status
|
||||
|
||||
Response models are verified against production (`sweep.mjs` records field-names+types
|
||||
only, no PII). **CONFIRMED live:** login, `UserDetail`, vehicles, cards,
|
||||
`Zone`/`Space`/`SpacePolicy`, parking lots, all three estimates, and — from a real DL-zone
|
||||
session — `PastSession` and `ParkingReceipt`. **Still UNCONFIRMED:** `ActiveSession` (no
|
||||
active session existed on the account at capture time). Re-run `sweep.mjs` (schema diff) or
|
||||
`capture-dl.mjs` (full DL request/response record) to refresh.
|
||||
|
||||
Not affiliated with or endorsed by IPS Group / ParkSmarter.
|
||||
|
|
@ -12,6 +12,11 @@ Node 18+. It has **zero runtime dependencies** (uses `fetch`). If you specifical
|
|||
want a native-Kotlin client instead, the `environments.ts` + endpoint table below map
|
||||
directly onto Retrofit/OkHttp — ask and it can be ported.
|
||||
|
||||
> **Usage guide:** for narrative walk-throughs (searching, quoting a price, running a
|
||||
> transaction) plus per-endpoint commentary and the behavioral quirks, see
|
||||
> **[`docs/PARKSMARTER_API.md`](../docs/PARKSMARTER_API.md)**. The tables below are the
|
||||
> quick reference.
|
||||
|
||||
## Install / build
|
||||
|
||||
```bash
|
||||
|
|
@ -170,19 +175,26 @@ Request shapes were recovered from the app code and are exact. Response models w
|
|||
- **CONFIRMED via live capture:** login (`AuthResponse`), `UserDetail`, `VehicleDetail`,
|
||||
`CreditCardDetail`, `Zone`/`Space`/`SpacePolicy`, `ParkingLot`/`ParkingLotDetail`, all
|
||||
three estimate responses (`ParkingDetail` price ladder), notification-settings envelope,
|
||||
states wrapper, the password-reset flow, and the shared `Response` envelope.
|
||||
- **UNCONFIRMED (no session history on the test account):** `ActiveSession`,
|
||||
`PastSession`, `ParkingReceipt`. Field names are from static analysis; capture from an
|
||||
account with at least one past/active session to confirm.
|
||||
states wrapper, the password-reset flow, the shared `Response` envelope, and — from a real
|
||||
DL-zone session — `PastSession` and `ParkingReceipt`.
|
||||
- **UNCONFIRMED:** `ActiveSession` (no *active* session existed on the account at capture
|
||||
time — field names are from static analysis; capture while parked to confirm).
|
||||
- `capture-dl.mjs` records a full DL-zone request/response set; `sweep.mjs` refreshes the
|
||||
PII-free schema skeletons.
|
||||
|
||||
Two behaviors worth knowing (both confirmed live):
|
||||
Behaviors worth knowing (all confirmed live — full list in
|
||||
[`docs/PARKSMARTER_API.md`](../docs/PARKSMARTER_API.md)):
|
||||
|
||||
1. **Meter search requires auth.** `/api/Meter` and `/api/MeterList` return **401** without a
|
||||
valid `Auth_Token`, despite being data lookups.
|
||||
2. **`Response` envelope + token refresh.** Most authenticated responses embed
|
||||
`Response: { Auth_Token, Message, Status }`. When `Response.Auth_Token` (or a top-level
|
||||
`Auth_Token`) is non-empty, it's a rolling refresh of your user token — the client stores
|
||||
it automatically.
|
||||
1. **200-on-error.** Failed login, declined payment, and some bad requests return **HTTP
|
||||
200** with `Response.Status: "Error"`. Check the envelope, not just the status code.
|
||||
2. **Meter/estimate search requires auth.** `/api/Meter`, `/api/MeterList`, and estimates
|
||||
**401** without a valid `Auth_Token`, despite being data lookups.
|
||||
3. **Rolling token refresh.** A non-empty `Response.Auth_Token` (or top-level) replaces your
|
||||
user token — the client stores it automatically.
|
||||
4. **Stale token ≠ 401.** An expired token makes `GET /api/User` return an **empty body**
|
||||
(not a 401); validate by requiring real user fields before trusting the session.
|
||||
5. **Estimate quirks.** Multi is unsupported on some zones (falls back to single);
|
||||
`MaxTime: 0`/`$0.00` means a free window; some zones are flat-rate.
|
||||
|
||||
To capture the remaining UNCONFIRMED models, re-run `sweep.mjs` on an account that has a
|
||||
saved card and session history. There's **no TLS pinning**, so a proxy capture on a rooted
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue