BigBrainParking/parksmarter-client
Erik 2b973348bc
All checks were successful
build-apk / build (push) Successful in 28m23s
v0.2.4: fix countdown notification not appearing + wrong time-remaining
Countdown notification "nothing appears" on session start:
- The small icon was the adaptive launcher mipmap (applicationInfo.icon),
  which Android 13+/GrapheneOS rejects as an invalid notification small icon
  and drops the post silently. Ship a proper white-on-transparent vector
  small icon (bbp_stat_parking) inside the module and use it.
- showCountdown now returns a diagnostic string (posted / notifications
  disabled / exception) instead of swallowing failures; JS logs the branch,
  permission result, and native outcome to the in-app diagnostics log.
- Log the raw active-session payload in refreshSessionStatus (element shape
  was previously unconfirmed).

Time remaining showed "7694 min":
- API's TimeRemaining is SECONDS, not minutes. Format it as hours+minutes
  in SessionDetail; annotate the type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:55:45 +00:00
..
src v0.2.4: fix countdown notification not appearing + wrong time-remaining 2026-07-15 21:55:45 +00:00
test privacy: remove personal test coordinate from committed docs/tests 2026-07-13 19:35:33 -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
capture-dl.mjs v0.1.7: collapse flat-rate zones to one option; add DL capture script 2026-07-13 10:58:49 -07:00
package-lock.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
package.json v0.1.5: detect free windows in the estimate fallback + add API tests 2026-07-12 20:42:58 -07:00
README.md docs: add ParkSmarter API usage guide + endpoint reference 2026-07-13 19:25:54 -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.

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. The tables below are the quick reference.

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, 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):

  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.

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