# 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

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

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