a neat project
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: update the readme

+159 -2
+159 -2
README.md
··· 1 - # huntington 1 + # huntington::neo 2 2 3 - neat project 3 + A native iOS client for Huntington Bank, built by reverse-engineering the private mobile API used by the official Android/iOS apps. 4 4 5 5 The canonical repo for this is hosted on tangled over at [`dunkirk.sh/huntington`](https://tangled.org/dunkirk.sh/huntington) 6 + 7 + ## API 8 + 9 + All API traffic goes through `m.huntington.com`. There are two namespaces: 10 + 11 + - `/api/mobile-authentication/1.8/` — auth flow (login, OTP, device registration) 12 + - `/api/mobile-customer-accounts/1.11/` — account data (balances, transactions) 13 + 14 + ### Authentication 15 + 16 + Every authenticated request requires two things: 17 + 18 + - Session cookies `PD-ID` and `PD-S-SESSION-ID` (set by IBM Security Verify / DataPower after login) 19 + - An `x-auth-receipt` header — a short-lived rolling token issued by the auth layer 20 + 21 + The receipt **rotates on every response**: each API call returns a new `x-auth-receipt` that must be used for the next call. Using a stale receipt yields a 401. 22 + 23 + #### Headers (all requests) 24 + 25 + | Header | Value | 26 + | ---------------- | -------------------------------------- | 27 + | `x-channel` | `MOBILE` | 28 + | `x-context-id` | UUID generated per session (lowercase) | 29 + | `x-auth-receipt` | Rolling receipt token | 30 + | `user-agent` | `HuntingtonMobileBankingIOS/6.74.115` | 31 + 32 + #### Login flow (new device / OTP required) 33 + 34 + ``` 35 + POST /api/mobile-authentication/1.8/mobile-init 36 + body: {} 37 + → 201 38 + 39 + POST /pkmslogin.form 40 + body: login-form-type=pwd&userName=...&password=... 41 + → 302 (sets PD-ID, PD-S-SESSION-ID cookies) 42 + 43 + GET /api/mobile-authentication/1.8/contexts/{ctx}/authentication-receipt 44 + ?olbLoginId={username}&loginType=USER_PASS 45 + → 200, x-auth-receipt header, body: { customerId } 46 + 47 + POST /api/mobile-authentication/1.8/contexts/{ctx}/second-factors 48 + body: { fingerprint, olbLoginId, policy: "ANDROID", profile: "MOBILE", 49 + deviceId, token, fraudSessionId, loginType, flowId } 50 + → 201, body: { secondFactorId, passed, registrationData } 51 + # passed=true → skip to activate-customer (trusted device) 52 + # passed=false → OTP required 53 + 54 + GET /api/mobile-authentication/1.8/contexts/{ctx}/second-factors/{sfId}/otp/delivery-options 55 + → 200, body: { phoneNumbers: [{id, value}], emailAddresses: [{id, value}] } 56 + 57 + PUT /api/mobile-authentication/1.8/contexts/{ctx}/second-factors/{sfId}/otp/delivery-options/{optionId} 58 + body: {} 59 + → 200 (sends OTP to selected phone/email) 60 + 61 + PUT /api/mobile-authentication/1.8/contexts/{ctx}/second-factors/{sfId}/otp/status 62 + body: { otpValue: "123456", flowId: "" } 63 + → 200, body: { passed: true }, rotates x-auth-receipt 64 + 65 + GET /api/mobile-authentication/1.8/contexts/{ctx}/second-factors/{sfId}/v2/ia-challenge-question 66 + → 200, body: {} (no challenge), rotates x-auth-receipt again 67 + 68 + POST /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/customers 69 + body: { secondFactorId, fraudSessionId } 70 + → 201, body: { customer: { customerId, name, ... } } 71 + 72 + POST /api/mobile-authentication/1.8/contexts/{ctx}/second-factors/{sfId}/registrations 73 + body: { deviceName: "iPhone" } 74 + → 201, body: { registrationData: { token } } 75 + # Save token — used in future second-factors calls to skip OTP 76 + ``` 77 + 78 + #### Login flow (trusted device, `passed=true`) 79 + 80 + Same as above through `second-factors`, then jump straight to `activate-customer`. No OTP, no `ia-challenge-question`. 81 + 82 + ### Account data 83 + 84 + ``` 85 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/accounts 86 + ?refresh=false 87 + → 200, body: { groups: [{ accountCategory, accounts: [{ accountId, accountType, 88 + nickName, availableBalance, currentBalance, 89 + maskedAccountNumber, routingNumber }] }] } 90 + 91 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/last-login 92 + → 200, body: { lastLogin: "2026-03-31T20:12:45.043Z" } 93 + 94 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/customer-contacts 95 + → 200, body: { baseContacts: { postalAddress, phoneNumbers, emailId }, 96 + alertContacts: { alertEmails, alertPhones } } 97 + 98 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/customer-custom-attribute 99 + → 200, body: feature flag map (UI state, onboarding flags, badge counts) 100 + ``` 101 + 102 + ### Transactions 103 + 104 + There are three transaction endpoints per account, each returning a different slice: 105 + 106 + ``` 107 + # Combined posted + pending (most recent page, no date filter) 108 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/deposits/{accountId}/transactions 109 + → 200, body: { items: [...] } 110 + # items have transactionCategory: "history" or "pending" 111 + # Returns a cursor in the last item for pagination (see below) 112 + 113 + # Paginate further back using a cursor from the previous response 114 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/deposits/{accountId}/transactions 115 + ?textRecordControl={cursor} 116 + → 200, body: { items: [...] } 117 + 118 + # Posted transactions only (savings/interest accounts use this endpoint) 119 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/deposits/{accountId}/transaction-history 120 + → 200, body: { items: [...] } 121 + 122 + # Pending transactions only 123 + GET /api/mobile-customer-accounts/1.11/contexts/{ctx}/customers/{customerId}/deposits/{accountId}/v2/pending-transactions 124 + → 200, body: { items: [...], inProcessTransactionExists, overdraftIndicator, 125 + idaResponse: { totalIdaAmount, defaultIdaAmount, ... } } 126 + ``` 127 + 128 + The `textRecordControl` cursor is an opaque string embedded in the transaction data — it encodes account number, account type, and date boundaries for the next page. Pass it verbatim to fetch the next batch. 129 + 130 + #### Posted transaction fields 131 + 132 + | Field | Description | 133 + | -------------------------------- | ----------------------------------------- | 134 + | `transactionCategory` | `"history"` or `"pending"` | 135 + | `transactionAmount` | Amount as string (always positive) | 136 + | `runningBalance` | Balance after this transaction | 137 + | `postedDate` | `YYYY-MM-DD` | 138 + | `payeeName` | Merchant/payee name (posted) | 139 + | `transactionTypeDescription` | e.g. `"Direct Deposit"`, `"Transfer"` | 140 + | `imageId` | Opaque ID (used as stable transaction ID) | 141 + | `referenceNumber` | Bank reference number | 142 + | `memos` | Array of memo strings | 143 + | `merchantCity` / `merchantState` | Card transaction location | 144 + | `oysa.isZelleTransaction` | Whether this is a Zelle transfer | 145 + 146 + #### Pending transaction fields 147 + 148 + | Field | Description | 149 + | ----------------------------------------- | ---------------------- | 150 + | `transactionType` / `transactionTypeDesc` | Type description | 151 + | `totalTransactionDebitAmount` | Debit amount (string) | 152 + | `postedTransactionCreditAmount` | Credit amount (string) | 153 + | `memo` | Memo string | 154 + 155 + ### Notes 156 + 157 + - The `x-context-id` UUID must be **lowercase** — uppercase UUIDs cause 500 errors on `otp/status` 158 + - `second-factors` must use `policy: "ANDROID"` — the `"IOS"` policy path has a server-side bug that causes 500 on `otp/status` 159 + - `pkmslogin.form` uses HTTP/2 and occasionally resets the connection (-1005); retry with a fresh context ID 160 + - Session state (context ID, receipt, customer ID, cookies) can be persisted and reused across app launches — validate by hitting the accounts endpoint on startup 161 + - The transactions endpoint returns a rolling window of recent items (not a fixed 30-day window); use `textRecordControl` pagination to go further back 162 + - The `transactions` endpoint mixes posted and pending; `transaction-history` and `v2/pending-transactions` split them out separately 6 163 7 164 <p align="center"> 8 165 <img src="https://raw.githubusercontent.com/taciturnaxolotl/carriage/main/.github/images/line-break.svg" />