# 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. ```