BigBrainParking/parksmarter-client
Hank fb3d1cdf3a Detect failed logins; fix map camera recenter, marker contrast, dark map tiles
- Client: server returns 200 + Status:"Error" on bad credentials; loginWith* now
  throw LoginError with the server message instead of faking a signed-in state
- Map: uncontrolled camera positioned once on load + explicit actions only, so
  Search/marker-tap/re-renders no longer snap back to the user's location
- Markers: always-dark high-contrast bubble + zone-colored dot (some zones report
  a white BackgroundColor -> was white-on-white)
- Dark mode now switches map tiles to CARTO dark-matter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 10:27:41 -07:00
..
src Detect failed logins; fix map camera recenter, marker contrast, dark map tiles 2026-07-06 10:27:41 -07:00
.creds.example.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
.gitignore Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
package-lock.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
package.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
README.md Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
sweep.mjs Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
tsconfig.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00

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.

Install / build

cd parksmarter-client
npm install
npm run build      # emits dist/

Import from source (src/index.ts) or the built dist/.

Quick start

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:

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, 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.

Two behaviors worth knowing (both confirmed live):

  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.

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.

For interoperability/research with your own account. Not affiliated with or endorsed by IPS Group / ParkSmarter. Respect their Terms of Service and applicable law.