Skip to content

Latest commit

 

History

History
705 lines (541 loc) · 20.2 KB

File metadata and controls

705 lines (541 loc) · 20.2 KB

NFCU Python API Reference

Reverse-engineered — This library targets the Navy Federal Credit Union mobile app API (Android v2026.2.1, digitalomni.navyfederal.org). It is an unofficial client; use it only for authorised access to your own accounts.


Table of Contents


Quick Start

from nfcu import NFCU

client = NFCU("your_username", "your_password")

# Step 1: initiate login, get MFA phone options
phone_options = client.login()
# [{"phoneNumber": "*1234", "phoneType": "M",
#   "phoneId": "cGhvbmUtaWQtcGxhY2Vob2xkZXI="}]

# Step 2: request OTP
client.request_otp()  # uses first option by default

# Step 3: enter OTP from SMS
client.submit_mfa(input("Enter OTP: "))

# Step 4: use the API
accounts = client.get_accounts()
for group in accounts["groups"].values():
    for acct in group["elements"]:
        attrs = acct["attributes"]
        name = attrs.get("alias", {}).get("value") or attrs.get("name", {}).get("value")
        bal  = attrs.get("bookedBalance", {}).get("value", "0")
        print(f"{name:<40} ${float(bal):>10,.2f}  {acct['id']}")

Authentication Flow

The NFCU mobile API uses a six-step authentication flow before any banking endpoints are accessible. Each step rotates the Bearer token.

Client                           Server
  |                                |
  |  GET  /api/auth/config/preauth |  ← Sets XSRF-TOKEN, prd_oar, ak_bmsc cookies
  |                                |
  |  POST /api/auth/mobile/authn   |
  |  {username, password,          |  ← Bearer token 1 in `authorization` header
  |   deviceFingerprint}           |
  |  x-acf-sensor-data: <akamai>  |
  |                                |
  |  GET  /api/auth/tfa/options    |  ← Returns list of phone numbers
  |                                |
  |  POST /api/auth/tfa/           |
  |       challenge/otp            |  ← Server sends SMS to chosen number
  |  {phoneId, otpType:"SMS"}      |
  |                                |
  |  POST /api/auth/tfa/           |
  |       challenge/verification   |  ← Bearer token 2 in `authorization` header
  |  {tfaType:"OTP", otp:"123456"} |
  |                                |
  |  GET  /api/auth/esi/activation |
  |  (called up to 3 times)        |  ← Bearer token 3 on new/high-risk devices
  |                                |    (verification token remains valid on known devices)
  |                                |
  |  POST /api/auth/tfa/decision   |  ← Optional risk assessment
  |  {eventId, denyRisk:true}      |
  |                                |
  |  ← Banking API now accessible  |

Token rotation: Three different Bearer tokens are issued across the auth flow. The library manages these automatically via _update_auth_state().

Akamai sensor data: The x-acf-sensor-data header is required on the authn request. Without it, Akamai's edge layer returns a synthetic LGN014 error before the request reaches NFCU's backend. The library ships a captured blob (EMULATOR_SENSOR_DATA in nfcu/fingerprint.py) that may expire over time; re-capture it with intercept/start.sh.


Class: NFCU

from nfcu import NFCU

Constructor

NFCU(
    username: str,
    password: str,
    device_fingerprint: str = EMULATOR_FINGERPRINT,
    device_metadata: dict | None = None,
    sf_device_id: str = EMULATOR_SF_DEVICE_ID,
    sensor_data: str = EMULATOR_SENSOR_DATA,
)
Parameter Type Description
username str NFCU online username
password str Account password
device_fingerprint str _v02 fingerprint string (see Device Fingerprint)
device_metadata dict | None JSON blob for x-nf-device-metadata header
sf_device_id str Stable per-device UUID for x-sf-device-id header
sensor_data str Akamai BM sensor blob for x-acf-sensor-data header

The constructor does not perform authentication. Call login() next.


login()

phones = client.login() -> list[dict]

Sends credentials and the device fingerprint. Returns the list of phone numbers eligible for OTP delivery.

Returns list[dict] — each entry:

{
  "phoneNumber": "*1234",
  "phoneType": "M",
  "phoneId": "cGhvbmUtaWQtcGxhY2Vob2xkZXI="
}
Field Description
phoneNumber Masked phone number
phoneType "M" (mobile) or "H" (home)
phoneId Base64 token passed to request_otp()

Raises NFCUAuthError on bad credentials.


request_otp()

result = client.request_otp(phone_id: str | None = None) -> dict

Asks the server to send an OTP SMS to the selected phone.

Parameter Default Description
phone_id None phoneId from login(). Defaults to first option.

Returns:

{"expiration": 360, "message": "Success"}

Raises NFCUAuthError if called before login().


submit_mfa()

result = client.submit_mfa(otp: str, remember_device: bool = False) -> dict

Completes the authentication flow by verifying the SMS OTP. Internally handles the ESI activation token rotation and TFA decision step.

Parameter Default Description
otp (required) 6-digit code from SMS
remember_device False Request server to remember device (requires device screen lock; emulator support limited)

Returns verification response:

{"name": "MARIE", "message": "Success", "token": "<bearer-token>"}

Raises NFCUMFAError on invalid or expired OTP.


get_accounts()

data = client.get_accounts() -> dict

Returns an overview of all accounts grouped by type.

Endpoint: GET /api/arrangement-manager/client-api/v2/arrangement-views/account-overview

Returns dict with groups (keyed by account type) and top-level metadata:

{
  "metadata": {
    "totalCount": 5,
    "balanceAggregations": { "...": "..." }
  },
  "groups": {
    "currentAccounts": {
      "elements": [
        {
          "id": "0a2c476a-d3bd-4a7a-9e1e-dca25ab0060a",
          "attributes": {
            "name":             {"value": "Flagship Checking"},
            "alias":            {"value": "Flagship Checking - 1107"},
            "bookedBalance":    {"value": "937.58"},
            "availableBalance": {"value": "895.45"}
          }
        }
      ],
      "metadata": {}
    },
    "savingsAccounts":     { "elements": [...], "metadata": {} },
    "creditCardsAccounts": { "elements": [...], "metadata": {} }
  }
}

Note: bookedBalance in the overview may show 0.00 for some accounts. Use get_account(id) for the authoritative per-account balance.

Example:

accounts = client.get_accounts()
for group in accounts["groups"].values():
    for acct in group["elements"]:
        attrs = acct["attributes"]
        name = attrs.get("alias", {}).get("value") or attrs.get("name", {}).get("value")
        bal  = attrs.get("bookedBalance", {}).get("value", "0")
        print(f"{name}  ${float(bal):,.2f}  {acct['id']}")

get_account()

detail = client.get_account(account_id: str) -> dict

Returns full arrangement details for one account.

Endpoint: GET /api/arrangement-manager/client-api/v2/arrangements/{account_id}

Parameter Description
account_id UUID from get_accounts()

Returns dict with fields including name, bookedBalance, availableBalance, BBAN (account number), currency, accountOpeningDate, debitCards, and more.


get_transactions()

txns = client.get_transactions(
    account_id: str,
    from_: int = 0,
    size: int = 25,
    state: str = "COMPLETED",
) -> list

Returns paginated transactions for an account, newest first.

Endpoint: GET /api/transaction-manager/client-api/v2/transactions

Parameter Default Description
account_id (required) UUID from get_accounts()
from_ 0 Zero-based page offset
size 25 Page size
state "COMPLETED" "COMPLETED" or "UNCOMPLETED" (pending)

Returns a plain list of transaction dicts (not wrapped in a dict):

[
  {
    "id": "tx-uuid",
    "arrangementId": "account-uuid",
    "bookingDate": "2026-02-27",
    "description": "AMAZON.COM",
    "creditDebitIndicator": "DBIT",
    "transactionAmountCurrency": {"amount": "42.99", "currencyCode": "USD"}
  }
]

Pagination example:

all_txns = []
offset = 0
while True:
    page = client.get_transactions(account_id, from_=offset, size=25)
    all_txns.extend(page)
    if len(page) < 25:
        break
    offset += 25

get_cards()

cards = client.get_cards() -> list

Returns all payment cards (debit and credit) on the account.

Endpoint: GET /api/cards-presentation-service/client-api/v2/cards

Returns a list of card dicts:

[
  {
    "id": "2e2d1d00-0a50-4fe2-85d5-fb3fb915c56c",
    "brand": "VS",
    "type": "Credit",
    "subType": "VSEC",
    "name": "cashRewards Secured Visa",
    "status": "Active",
    "lockStatus": "UNLOCKED",
    "maskedNumber": "2751",
    "additions": {
      "productDescription": "cashRewards Secured Visa - 2751",
      "ccAcctId": "3857431",
      "spentThisPeriod": "1599.42",
      "role": "Primary Cardholder"
    }
  },
  {
    "id": "9c31ed33...",
    "brand": "VS",
    "type": "Debit",
    "name": "Debit Card",
    "status": "Active",
    "maskedNumber": "9774",
    "additions": { "productDescriptionChkAcct": "Flagship Checking - 1107" }
  }
]
Field Description
id Card UUID (use with get_card() and get_card_rewards())
brand "VS" (Visa), "MC" (Mastercard), "SK" (debit)
type "Credit" or "Debit"
status "Active" or "Inactive"
maskedNumber Last 4 digits
additions.ccAcctId Legacy credit card account ID in the core banking system

get_card()

card = client.get_card(card_id: str) -> dict

Returns detailed information for a single payment card.

Endpoint: GET /api/cards-presentation-service/client-api/v2/cards/{card_id}

Parameter Description
card_id UUID from get_cards()

Returns a single card dict with the same structure as entries from get_cards().


get_card_rewards()

rewards = client.get_card_rewards(card_id: str) -> dict

Returns cash-back rewards information for a credit card.

Endpoint: GET /api/cards-presentation-service/client-api/v2/rewards/{card_id}

Parameter Description
card_id UUID of the credit card (from get_cards())

Returns:

{
  "reward_acct_id": "00003857431",
  "eligible": true,
  "balance": "6.22",
  "currency": "CASH",
  "target_accounts": [
    {
      "id": "0a2c476a-d3bd-4a7a-9e1e-dca25ab0060a",
      "account_number": "1107",
      "available_balance": "895.45000",
      "product_description": "Flagship Checking - 1107"
    }
  ]
}
Field Description
reward_acct_id Legacy reward account ID in the core banking system
eligible Whether redemption is currently available
balance Current cash-back balance as a string
currency "CASH" for cash-back cards
target_accounts Accounts eligible to receive a cash-back deposit

get_user()

me = client.get_user() -> dict

Returns profile information for the authenticated member.

Endpoint: GET /api/user-manager/client-api/v2/users/me

Returns dict with fields including fullName, email, membershipStatus, and member-number details.


get_messages_indicator()

indicator = client.get_messages_indicator() -> dict

Returns the unread secure-message count.

Endpoint: GET /api/message-manager/client-api/v1/messages/indicator

Returns: {"unreadCount": 3}


logout()

client.logout()

Invalidates the server-side session and clears all local tokens. The client object should not be used after this call.


Device Fingerprint

Format

The deviceFingerprint field in every login request is a custom encoding:

_v02 + base64( XOR(plaintext, 0x55) )

The plaintext is a URL query string:

fpdt=2&mfos=Android&mfov=14&...&mfec=<rsa-signature>

Parameters

Parameter Example Description
fpdt 2 Fingerprint data type
mfos Android OS name
mfov 14 OS version
mfwa 02:00:00:00:00:00 WiFi MAC address
mfsc 2209|1080 Screen size (width|height px)
fpln en_US Device locale
mfgc 00.0000|00.0000 GPS coordinates (lat|lon)
mfpv 2 Protocol version
fpts 1772171439981 Unix timestamp in milliseconds
mfappid com.navyfederal.android App bundle ID
mfa_isrooted false Root detection result
mfa_id 3212038e3f2f8791 Android hardware ID
mfa_bd goldfish_arm64 Build.DEVICE
mfa_br google Build.BRAND
mfa_ca1 arm64-v8a Primary CPU ABI
mfa_fp google/sdk_gphone64_arm64/... Build.FINGERPRINT
mfa_dv emu64a Device variant
mfa_dp sdk_gphone64_arm64-userdebug... Full build description
mfa_mf Google Build.MANUFACTURER
mfa_md sdk_gphone64_arm64 Build.MODEL
mfa_tags dev-keys Build.TAGS
mfa_bl unknown Build.BOOTLOADER
mfa_hw ranchu Build.HARDWARE
mfa_sci us SIM country ISO
mfa_spn T-Mobile SIM carrier name
mfa_so 310260 SIM operator numeric
mfec Q2Pm...CQ== RSA-2048 signature (base64)

The mfec Signature

The mfec parameter is a 256-byte (2048-bit) RSA signature over all preceding parameters (the query string before &mfec=). The private key is embedded in the NFCU Android APK.

Because the timestamp (fpts) is part of the signed payload, the signature changes on every request. Without the APK private key, fresh signatures cannot be generated.

The library ships with a captured emulator fingerprint (EMULATOR_FINGERPRINT in nfcu/fingerprint.py) that can be used for development/testing. The server accepts it with the stale timestamp because the device is already registered.

Providing Your Own Fingerprint

To capture a real device fingerprint:

  1. Run intercept/start.sh (see intercept/SETUP.md)
  2. Install the NFCU app and log in through the emulator
  3. Find the POST /api/auth/mobile/authn flow in mitmweb (localhost:8081)
  4. Extract the deviceFingerprint value from the request body
  5. Pass it to NFCU(..., device_fingerprint=your_fp)

To decode any fingerprint for inspection:

from nfcu.fingerprint import decode
plaintext = decode("_v02MyUx...")
print(plaintext)
# fpdt=2&mfos=Android&mfov=14&...

Exceptions

Exception Inherits When raised
NFCUAuthError Exception Bad credentials or missing auth state
NFCUSessionExpiredError NFCUAuthError HTTP 401 after token was set
NFCUMFAError Exception Invalid or expired OTP
NFCURateLimitError Exception HTTP 429 Too Many Requests
NFCUAPIError Exception Any other non-2xx response

NFCUAPIError exposes status_code and body attributes:

from nfcu.exceptions import NFCUAPIError

try:
    client.get_accounts()
except NFCUAPIError as e:
    print(e.status_code, e.body[:100])

Known Account IDs

These UUIDs were observed during traffic capture on 2026-02-27 for a specific member account. They will differ for all members.

Account Display Name UUID
Flagship Checking Flagship Checking - 1107 0a2c476a-d3bd-4a7a-9e1e-dca25ab0060a
Easy Checking Easy Checking - 7514 f9fbaf68-95bf-4d32-8901-9c1519d6a951
Savings (6277) Membership Share Savings - 6277 31a9b8fe-7487-4cb7-8c32-ffd3c33903bf
Savings (8330) Membership Share Savings - 8330 4e345276-78f2-42c6-98c6-46a90737809a
cashRewards Visa cashRewards Secured Visa - 2751 2e2d1d00-0a50-4fe2-85d5-fb3fb915c56c

Account IDs are available at runtime via get_accounts()["groups"][...]["elements"][n]["id"].


HTTP Headers Reference

Every request after authentication must include:

Header Value Notes
authorization Bearer <token> Rotated across auth steps
x-xsrf-token UUID Must match XSRF-TOKEN cookie (double-submit CSRF pattern)
x-nf-profile-tag 32 char alphanumeric Generated client-side; server may update it
x-sf-device-id UUID Stable per-device identifier
x-nf-device-metadata Base64 JSON See below
cid Mobile Client identifier
platform AND Platform code for Android
appversion 2026.2.1 Must match current app version
user-agent NavyFederal/2026.2.1 (Android 14)
content-type application/json

x-nf-device-metadata decodes to:

{
  "name": "Google",
  "model": "sdk_gphone64_arm64",
  "platform": "AND",
  "multitask": true,
  "systemName": "Android",
  "systemVersion": "14",
  "screenSize": "2209x1080",
  "language": "en",
  "ipAddress": "10.0.2.15",
  "hardwareId": "3212038e3f2f8791"
}

API Endpoints Reference

All endpoints are under https://digitalomni.navyfederal.org.

Authentication

Method Path Description
GET /api/auth/config/preauth Initialise session; sets XSRF-TOKEN cookie
POST /api/auth/mobile/authn Username + password + device fingerprint
GET /api/auth/tfa/options MFA phone options
POST /api/auth/tfa/challenge/otp Request SMS OTP
POST /api/auth/tfa/challenge/verification Verify OTP
GET /api/auth/esi/activation Token rotation (call up to 3×)
POST /api/auth/tfa/decision Risk assessment decision
GET /api/auth/logout Invalidate session

Accounts

Method Path Description
GET /api/arrangement-manager/client-api/v2/arrangement-views/account-overview All accounts + balances grouped by type
GET /api/arrangement-manager/client-api/v2/arrangements/{id} Single account detail

Transactions

Method Path Description
GET /api/transaction-manager/client-api/v2/transactions?arrangementId={id}&from={n}&size={n}&state={s} Paginated transactions (returns plain list)

Cards

Method Path Description
GET /api/cards-presentation-service/client-api/v2/cards All payment cards (debit + credit)
GET /api/cards-presentation-service/client-api/v2/cards/{id} Single card detail
GET /api/cards-presentation-service/client-api/v2/rewards/{id} Card rewards balance + eligible target accounts

User & Messaging

Method Path Description
GET /api/user-manager/client-api/v2/users/me Member profile
GET /api/message-manager/client-api/v1/messages/indicator Unread message count