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.
- Quick Start
- Authentication Flow
- Class:
NFCU - Device Fingerprint
- Exceptions
- Known Account IDs
- HTTP Headers Reference
- API Endpoints Reference
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']}")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-dataheader is required on the authn request. Without it, Akamai's edge layer returns a syntheticLGN014error before the request reaches NFCU's backend. The library ships a captured blob (EMULATOR_SENSOR_DATAinnfcu/fingerprint.py) that may expire over time; re-capture it withintercept/start.sh.
from nfcu import NFCUNFCU(
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.
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.
result = client.request_otp(phone_id: str | None = None) -> dictAsks 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().
result = client.submit_mfa(otp: str, remember_device: bool = False) -> dictCompletes 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.
data = client.get_accounts() -> dictReturns 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:
bookedBalancein the overview may show0.00for some accounts. Useget_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']}")detail = client.get_account(account_id: str) -> dictReturns 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.
txns = client.get_transactions(
account_id: str,
from_: int = 0,
size: int = 25,
state: str = "COMPLETED",
) -> listReturns 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 += 25cards = client.get_cards() -> listReturns 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 |
card = client.get_card(card_id: str) -> dictReturns 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().
rewards = client.get_card_rewards(card_id: str) -> dictReturns 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 |
me = client.get_user() -> dictReturns 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.
indicator = client.get_messages_indicator() -> dictReturns the unread secure-message count.
Endpoint: GET /api/message-manager/client-api/v1/messages/indicator
Returns: {"unreadCount": 3}
client.logout()Invalidates the server-side session and clears all local tokens. The client object should not be used after this call.
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>
| 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 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.
To capture a real device fingerprint:
- Run
intercept/start.sh(see intercept/SETUP.md) - Install the NFCU app and log in through the emulator
- Find the
POST /api/auth/mobile/authnflow in mitmweb (localhost:8081) - Extract the
deviceFingerprintvalue from the request body - 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&...| 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])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"].
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"
}All endpoints are under https://digitalomni.navyfederal.org.
| 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 |
| 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 |
| Method | Path | Description |
|---|---|---|
GET |
/api/transaction-manager/client-api/v2/transactions?arrangementId={id}&from={n}&size={n}&state={s} |
Paginated transactions (returns plain list) |
| 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 |
| 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 |