Transit Cards
Transit balance readers probe known public application paths and return a normalized TransitBalance.
let manager = NFCSessionManager()
defer { manager.invalidate() }
let (info, transport) = try await manager.scan()
let balance = try await CoreExtendedNFC.readTransitBalance(info: info, transport: transport)
Pass the original info and transport from NFCSessionManager.scan directly to the reader. Do not call refineCardInfo first: its probes can change the selected application. CoreExtendedNFC.scan performs that refinement automatically, so use NFCSessionManager.scan for transit reads.
FeliCa cards are selected by system code. The FeliCa common area (FE00) can hold several prepaid e-money purses: Rakuten Edy, WAON and nanaco are tried in that order, each recognized by its balance service. ISO 7816 cards are probed for CEPAS with SELECT file 4000 and GET PURSE, then for KS X 6924 and T-Union.
Detection Fallback
- CEPAS: any status-word rejection of SELECT
4000, or GET PURSE answered with6A81,6A82,6D00or6E00, means "not CEPAS" and the next reader runs. Communication, malformed-response and history errors are thrown. - Rakuten Edy, WAON and nanaco: a common-area card without the purse's balance service (Request Service answers
FFFF) throws.tagNotSupported, and the next purse is tried. Once the balance service exists, a refused balance read is thrown; refused card-number, points or date blocks are left out of the result, and the first refused history block ends the history. - KS X 6924 and T-Union: only
NFCError.applicationNotFound(aid:)(no AID selectable) or.tagNotSupportedmoves on. Once an AID is selected, a rejected GET BALANCE or any other failure is thrown. - Session loss and task cancellation always end the read. If no reader recognizes the card,
NFCError.tagNotSupportedis thrown.
| Card family | Selector | Reader |
|---|---|---|
| Japan IC | FeliCa 0003, service 008B | Balance and history |
| Rakuten Edy | FeliCa FE00, service 1317 | Balance, Edy ID, issue date and history |
| WAON | FeliCa FE00, service 6817 | Balance, WAON number, points and history |
| nanaco | FeliCa FE00, service 5597 | Balance, nanaco number, card dates, points and history |
| Octopus | FeliCa 8008, service 0117 | Balance |
| Singapore EZ-Link / CEPAS | File 4000, purse 3 | Balance and recent history |
| Korea KS X 6924 | Hyundai, T-Money / Snapper, Cashbee, MOIBA, EB Card, K-Cash AIDs | Balance and records where available |
| China T-Union | AID A000000632010105 | Dual-purse balance and records |
Amounts and Balances
TransitTransaction.amountis always an unsigned magnitude. Usetypefor direction:.topupadds,.tripand.purchasededuct.- T-Union: the displayed balance is the primary purse minus the negative purse, in fen. A card that rejects the negative-purse command with a status word has no negative purse (treated as 0); other failures are thrown. Transaction types
01and02are top-ups. - Snapper: Wellington Snapper cards answer the T-Money AID. When every SFI 4 record carries Snapper's
FFfiller, the card is reported as Snapper in NZD cents. Snapper trip records are not decoded. - Rakuten Edy, WAON and nanaco: balances and amounts are in yen, and dates are Japan Standard Time.
serialNumberis the number printed on the card (Edy ID, WAON number or nanaco number), or the IDm when that block cannot be read; the IDm is always inmetadata["idm"]. WAON and nanaco point balances are inmetadata["points"]. Edy and nanaco issue dates becomevalidFrom, and the nanaco last-use date is inmetadata["lastUseDate"]. Payments are purchases and charges top-ups; Edy gifts and nanaco point exchanges are top-ups. Other types, including nanaco transfers (metadata["event"] == "transfer"), are.unknownwith no amount. WAON records keep both raw amounts inmetadata["usageAmount"]andmetadata["chargeAmount"], and are sorted newest first because the card does not store them in date order. Tested with ACR122U captures replayed through these readers: Edy balance and six purchase records, nanaco balance, points and five purchase records, plus two WAON cards: Apple Wallet balance and three purchase records, and a 239-yen balance, 0 points and three purchase records. CardBal's Edy, WAON and nanaco decoders have also been tested on real cards; see the research notes. - Octopus: balance in HKD cents is
(raw − 350) × 10. Sources disagree on the offset (Metrodroid and FareBot use 500 for scans after 2017-10-01), and 350 has not been confirmed on a current physical card; createOctopusReader(transport:balanceOffset:)to use another offset. See the offset note. - CEPAS: timestamps count from 1995-01-01 00:00 Singapore time (UTC+08:00). Transaction types follow Metrodroid:
30/31(MRT, bus) are trips,03/75top-ups and76(bus refund) a top-up,01(retail) and04(service charge) purchases, each with its amount. Other types keep their raw payload and have no amount. - Japan IC history follows Metrodroid
SuicaTrip: rail gate records have an entry and an exit station; ticket-machine and fare-adjustment records (console types03,07,08,12-15) have onlyentryStation, andexitStationisnil; bus records (console type05) have no stations and carry the line and stop codes inmetadata["busLineCode"]andmetadata["busStopCode"]. Records with stations also carry the region of each station (byte 15: entry bits 7-6, exit bits 5-4) inmetadata["entryRegion"]andmetadata["exitRegion"]("0"-"3"); a station code is unique only together with its region. Product sales and charges carry no stations. Process types02h,07h,14h,15h,17h(bus auto-charge),1Fh,48hand49hare top-ups. History slots whose date field (bytes 4-5) is zero are skipped, as in MetrodroidSuicaTransitData; records with an out-of-range date or purchase time are kept withdate == nil.
Singapore EZ-Link / CEPAS
Traditional, non-SimplyGo CEPAS cards are included in readTransitBalance and the example app’s Scan Transit Card action. Reads return the purse and recent transaction log. A single traditional EZ-Link card was read successfully on iPhone Air running iOS 27.0 (24A5355q) on 2026-09-25: the app displayed its balance and four history records. That test used the dedicated Singapore path; the integrated automatic scan has not yet been tested on a physical card. Offline operation, ticket-machine comparison, and other card variants have not been verified. SimplyGo account-based history is not supported.
The example app’s ISO 7816 AID list already contains A000000341000101 as an existing unverified candidate. The actual initial AID from the successful read has not been confirmed, so that read does not verify this candidate. Preserve the initial AID in exported Logs when testing another card.
The reader uses the standard five-byte GET PURSE encoding accepted by Core NFC and the tested card. Older cards may reject it with 6700; the historical six-byte variant is rejected by NFCISO7816APDU(data:) in the iOS Simulator. See the protocol findings.
Physical Card Verification
- Use one traditional EZ-Link card that has not been upgraded to SimplyGo. Photograph or record its ticket-machine balance and two known recent journeys, including their dates, times, amounts, and stations or bus routes.
- Run the example app on an NFC-capable iPhone. Turn off Wi-Fi and cellular data, then open Tools → Transit Balance → Scan Transit Card. Keep only that card near the phone.
- Compare the displayed balance and recent records with the reference. Singapore transaction times are shown in Singapore time and monetary amounts in SGD. Raw monetary values use SGD cents. Records without a recognized monetary meaning retain their raw type and payload but omit an amount. CEPAS logs do not provide a post-transaction balance, so it is omitted.
- Open Logs and export the existing text/JSON log bundle. It includes session outcome, initial AID when detected, and APDU traffic. Record the iPhone model, iOS version, card type, and reference journeys with the test result.
- Record detection failures, protocol errors, and mismatches as failures. Distinguish a successful read from a fully verified result: an offline read must match the reference balance and both known journeys before marking that comparison complete. Simulator and mock tests cannot establish physical-card support.
See Transit Card Research for provenance.