- 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>
207 lines
9.4 KiB
Markdown
207 lines
9.4 KiB
Markdown
# parksmarter-client
|
|
|
|
An unofficial, typed **TypeScript** client for the ParkSmarter (IPS Group) parking
|
|
API. It was reverse-engineered from the official Android app
|
|
`com.ipsgroupinc.parksmarter` 4.4.0 — a React Native / Hermes build — by decompiling
|
|
the JS bundle and recovering every endpoint path, header, app token, and
|
|
request/response field name.
|
|
|
|
TypeScript was chosen because the target is "an alternative Android app or web app":
|
|
the same package runs unchanged in a browser, in React Native (Expo/bare), and in
|
|
Node 18+. It has **zero runtime dependencies** (uses `fetch`). If you specifically
|
|
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
|
|
cd parksmarter-client
|
|
npm install
|
|
npm run build # emits dist/
|
|
```
|
|
|
|
Import from source (`src/index.ts`) or the built `dist/`.
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import { ParkSmarterClient } from './src';
|
|
|
|
const ps = new ParkSmarterClient({ environment: 'prodv2' }); // default env
|
|
|
|
// 1) Bootstrap — seeds a ParkSmarter_SessionId and returns feature flags.
|
|
const validity = await ps.getApplicationValidity();
|
|
if (validity.Config?.IsInMaintenanceMode) throw new Error('maintenance');
|
|
|
|
// 2) Log in — Auth_Token + SessionId are stored automatically.
|
|
await ps.loginWithPhone({ phoneNumber: '5551234567', password: 'hunter2' });
|
|
|
|
// 3) Use authenticated endpoints.
|
|
const me = await ps.getUserDetail();
|
|
const nearby = await ps.getMetersByLocation({ latitude: 40.44, longitude: -79.99 });
|
|
|
|
// 4) Start a session.
|
|
await ps.startParkingSession({
|
|
creditCardId: me.CreditCardDetails![0].CCID!,
|
|
vehicleId: me.VehicleDetails![0].VehicleID!,
|
|
zoneId: nearby.Zones![0].ZoneId!,
|
|
spaceId: nearby.Zones![0].Spaces![0].SpaceID!,
|
|
customerId: nearby.Zones![0].CustomerID!,
|
|
meterTypeId: nearby.Zones![0].MeterTypeId!,
|
|
startTime: new Date().toISOString(),
|
|
endTime: new Date(Date.now() + 3600_000).toISOString(),
|
|
minutesToPurchase: 60,
|
|
parkingCost: 2.0,
|
|
transactionFee: 0.35,
|
|
});
|
|
```
|
|
|
|
### React Native / Node < 18
|
|
|
|
Pass a `fetchImpl` if the global `fetch` isn't present, and use persistent storage
|
|
for tokens:
|
|
|
|
```ts
|
|
import { ParkSmarterClient, TokenStore } from './src';
|
|
import * as SecureStore from 'expo-secure-store';
|
|
|
|
const tokens: TokenStore = {
|
|
getAuthToken: () => SecureStore.getItemAsync('ps_auth'),
|
|
setAuthToken: (t) =>
|
|
t ? SecureStore.setItemAsync('ps_auth', t) : SecureStore.deleteItemAsync('ps_auth'),
|
|
getSessionId: () => SecureStore.getItemAsync('ps_session'),
|
|
setSessionId: (s) =>
|
|
s ? SecureStore.setItemAsync('ps_session', s) : SecureStore.deleteItemAsync('ps_session'),
|
|
};
|
|
|
|
const ps = new ParkSmarterClient({ tokens /*, fetchImpl: fetch */ });
|
|
```
|
|
|
|
## Authentication model
|
|
|
|
This API does **not** use OAuth/Bearer. Auth is carried in custom headers:
|
|
|
|
| Header | Meaning | Source |
|
|
| --- | --- | --- |
|
|
| `Application_Token` | Identifies the app build. Required on **every** request. | Per-environment constant (baked in). |
|
|
| `X-Request-Id` | Fresh UUID per request. | Generated by the client. |
|
|
| `Auth_Token` | The logged-in user's token. | Returned in the **body** of `POST /api/Auth` as `Auth_Token`; sent as a header on authenticated calls. |
|
|
| `ParkSmarter_SessionId` | Server session id. | Seeded by `GET /api/ApplicationValidity` and login; echoed on some responses. |
|
|
| `Content-Type: application/json` | On POST/PUT only. | — |
|
|
|
|
The client persists `Auth_Token` and `SessionId` into the `TokenStore` automatically.
|
|
Every request also carries a `localeCode` query param (default `en`).
|
|
|
|
> Note: a `POST /api/Auth` returning HTTP **201** is treated by the app as a
|
|
> "needs verification / not a success" case, not a normal login. Handle 201 distinctly
|
|
> if you see it.
|
|
|
|
## Environments
|
|
|
|
| Name | Base URL | Notes |
|
|
| --- | --- | --- |
|
|
| `dev` | `https://dev-parksmarter-api.ipsmeters.com` | |
|
|
| `stage` | `https://staging-parksmarter-api.ipsmeters.com` | |
|
|
| `test` | `https://testing-parksmarter-api.ipsmeters.com` | |
|
|
| `prodv1` | `https://api.parksmarter.com` | |
|
|
| `prodv2` | `https://apiv2.parksmarter.com` | **default** |
|
|
| `prodv3` | `https://apiv3.parksmarter.com` | |
|
|
|
|
Application tokens for each are in `src/environments.ts`.
|
|
|
|
## Endpoint reference
|
|
|
|
Every method maps to one endpoint. Method is inferred from the app's naming.
|
|
|
|
| Client method | HTTP | Path |
|
|
| --- | --- | --- |
|
|
| `getApplicationValidity()` | GET | `/api/ApplicationValidity` |
|
|
| `loginWithPhone()` / `loginWithApple()` | POST | `/api/Auth` |
|
|
| `logoutAllDevices()` | POST | `/api/Auth/Logout` |
|
|
| `signUp()` | POST | `/api/User` |
|
|
| `getUserDetail()` | GET | `/api/User` |
|
|
| `updateProfile()` | PUT | `/api/User` |
|
|
| `requestDeleteUser()` | DELETE | `/api/User` |
|
|
| `isEmailRegistered()` / `isPhoneRegistered()` | GET | `/api/User` |
|
|
| `requestResetPassword()` | POST | `/api/Password` |
|
|
| `updatePassword()` | PUT | `/api/Password` |
|
|
| `requestVerifyUser()` | POST | `/api/UserVerification` |
|
|
| `verifyUser()` | GET | `/api/UserVerification` |
|
|
| `updateDeviceToken()` | PUT | `/api/Device` |
|
|
| `addVehicle()` | POST | `/api/Vehicle` |
|
|
| `updateVehicle()` | PUT | `/api/Vehicle` |
|
|
| `deleteVehicle()` | DELETE | `/api/Vehicle` |
|
|
| `addCard()` / `updateCard()` | POST | `/api/Card` |
|
|
| `setCardDefault()` | PUT | `/api/Card` |
|
|
| `deleteCard()` | DELETE | `/api/Card` |
|
|
| `getMetersByLocation()` | GET | `/api/Meter` (`Lat`,`Long`) |
|
|
| `getLimitedMetersByLocation()` | GET | `/api/MeterList` (`Lat`,`Long`) |
|
|
| `getMetersByZoneName()` / `searchMetersByZoneOrSpace()` | GET | `/api/Meter` (`ZoneName`/`Query`) |
|
|
| `getMetersBySerialNumber()` / `getMetersByScannerCode()` | GET | `/api/Meter` (`TerminalSerNo`/`ScannerCode`) |
|
|
| `getParkingLots()` | GET | `/api/ParkingLogix` |
|
|
| `getParkingEstimateMulti()` | GET | `/api/ParkingEstimateMulti` |
|
|
| `getParkingEstimateSingle()` | GET | `/api/ParkingEstimate` |
|
|
| `getParkingEstimateItems()` | GET | `/api/ParkingEstimateItems` |
|
|
| `startParkingSession()` | POST | `/api/Session` |
|
|
| `getActiveParkingSessions()` | GET | `/api/ParkingSession` |
|
|
| `getPastParkingSessions()` | GET | `/api/Session` |
|
|
| `getParkingReceipt()` | GET | `/api/ParkingReceipt` |
|
|
| `emailParkingReceipt()` | POST | `/api/ParkingReceipt` |
|
|
| `getNotificationSettings()` | GET | `/api/Setting` |
|
|
| `setNotificationSettings()` | POST | `/api/Setting` |
|
|
| `getStates()` | GET | `/api/State` |
|
|
| `getAbout()` / `getFAQ()` / `getPrivacyPolicy()` / `getTerms()` | GET | `/api/ParkSmarter*` |
|
|
|
|
## Field-name conventions
|
|
|
|
The server uses PascalCase ("PSJSON"). This client accepts friendly camelCase inputs
|
|
and maps them to the wire format for you; **responses are returned as the raw server
|
|
PascalCase JSON** and typed accordingly in `src/types.ts`. Response interfaces include
|
|
an index signature because not every optional field is guaranteed on every call — the
|
|
well-known fields are typed explicitly.
|
|
|
|
## Accuracy & verification status
|
|
|
|
Request shapes were recovered from the app code and are exact. Response models were then
|
|
**verified against production** by logging in and calling every read-only endpoint (see
|
|
`sweep.mjs`, which records field-names+types only — no PII). Each interface in
|
|
`src/types.ts` is annotated CONFIRMED or UNCONFIRMED.
|
|
|
|
- **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, 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.
|
|
|
|
Behaviors worth knowing (all confirmed live — full list in
|
|
[`docs/PARKSMARTER_API.md`](../docs/PARKSMARTER_API.md)):
|
|
|
|
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
|
|
device/emulator is also an option if you'd rather see the app's own traffic.
|
|
|
|
## Legal
|
|
|
|
For interoperability/research with your own account. Not affiliated with or endorsed by
|
|
IPS Group / ParkSmarter. Respect their Terms of Service and applicable law.
|
|
```
|