How to Read SUICA Information with CoreNFC (FeliCa)
Introduction
Starting from iOS 11.0, developers could read and write NFC tags via CoreNFC, but reading IC cards wasn’t supported until iOS 13 opened up IC card reading.
I’ve always been interested in NFC and wanted to try reading Suica (a Japanese transit IC card) data myself, so that I could check my balance directly with my phone (I know there are already apps that do this). However, there are relatively few Chinese resources online about reading Suica, so I spent a few days studying the FeliCa documentation and implementing it using CoreNFC.
This article starts with the NFC protocol, moves on to FeliCa—which is widely used in Japanese transit IC cards—and finally covers the implementation in Swift. Since I’m still relatively new to Swift, some parts of the code might not be optimal.
What is NFC?
NFC (Near Field Communication) is a short-range communication protocol and also an RFID wireless communication technology.
This protocol mainly defines:
- Communication protocol: How the transmitter and receiver communicate with each other
- Data exchange: How the two exchange data
This article focuses on the process of reading data from FeliCa.
The Problem NFC Solves
For wireless communication, we can use Bluetooth, Wi-Fi, etc., but the biggest challenges lie in security and pairing.
Bluetooth often requires pairing before devices can talk to each other, making the setup somewhat tedious. Furthermore, if a card reader could detect card information or request payments from 20 meters away, it would pose significant security risks.
Therefore, in NFC, the detection distance is typically within a few centimeters. In addition to ensuring security, this also minimizes noise and interference.
FeliCa
FeliCa is a contactless IC card technology developed by Sony in 2001. It is faster than NFC Type-A and Type-B, likely owing to Japan’s massive commuter crowds.
As a side note, Taiwan’s EasyCard is an IC card made using Mifare, designed by Philips. Mifare is also widely used around the world for contactless IC cards, whereas FeliCa seems to be predominantly adopted in Japan.
FeliCa is remarkably fast. If you’ve taken public transit in Japan, you’ve probably noticed that people don’t need to pause at all when passing through ticket gates. Widely used transit IC cards such as Suica, ICOCA, and Hayakaken all adopt FeliCa technology. Beyond transit, these IC cards are also frequently used for payments at convenience stores.
Data Security
In NFC, some data is readable while some is not; certain cards require the correct decryption keys to read and write data.
Although both Android and iOS can now read NFC cards, tampering with data like balances still requires valid cryptographic keys.
FeliCa Architecture
The FeliCa architecture is broadly divided into two areas: the Private Area (プライベート領域) and the Common Area (共通領域).
The Common Area stores readable information, while the Private Area holds personal details or balance controls, requiring cryptographic authentication to access.
The Common Area is further divided into several components:
- System: Represents the card unit. Every card has a 2-byte system code. According to Japan’s JIS 6319-4 standard, this value falls between FE00 and AA00. For cards like Suica, it is
0003(this value is crucial and will appear later). - Area: Contains information such as storage space and the number of blocks allocated to each service.
- Service: Stores data in blocks for external access. Each service has a 2-byte service code; for example, the transit history service code is
090f. Services are categorized into Random Service, Cyclic Service, and Pass Service. - Block: Where the actual data is stored. Each block is 16 bytes, and the required number of blocks varies depending on the service.
If you are just reading data, the parts you’ll encounter most often are the service and the block.
Types of Services
As mentioned earlier, services are categorized into Random, Cyclic, and Pass, primarily based on their data access patterns:
- Random Service: Free read/write access, holding data defined by the manufacturer.
- Cyclic Service: Stores historical/log data.
- Pass Service: Manages balances, reloads, and deductions.
Commands
FeliCa supports various commands, which can be found in the FeliCa documentation. To read FeliCa data, several commands are required:
- Polling
- Request Service
- Read Without Encryption
Each command has a corresponding request packet specifying what the request must contain. CoreNFC already provides built-in methods wrapping these commands.
FeliCa Communication Flow
- Capture the card using the Polling command.
- Send the Request Service command with a list of service codes. The card verifies whether the service codes are valid and accessible. If a service does not exist or is invalid, it returns
0xFFFF. - Send the Read Without Encryption command along with the service code. The card returns the corresponding data in blocks (up to 16 services).
- This command returns
status1andstatus2. If both are0, the operation succeeded. - Non-zero values indicate errors; since there are quite a few error codes, I won’t list them all here.
- This command returns
iOS Implementation
For instructions on reading general NFC tags, refer to Apple Developer’s sample code. Here, we will focus specifically on how to read a FeliCaTag.
To test NFC, you must use a physical iPhone, as the iOS Simulator cannot read NFC.
You can check whether NFC reading is supported using NFCTagReaderSession.isReadingAvailable.
First, consult the CoreNFC documentation to configure your Info.plist and entitlements.
1. Add Near Field Communication Tag Reading to Capabilities

2. Add ISO18092 system codes for NFC Tag Reader Session to Info.plist

- This corresponds to the system code mentioned earlier; for cards like Suica, it is
0003. - Add
Privacy - NFC Scan Usage Description.
CoreNFC Implementation
In NFCTagReaderSession, methods for polling, requestService, and readWithoutEncryption are already wrapped for you, so you don’t have to assemble raw bytes and craft low-level commands by hand.
However, it’s still essential to understand the general flow:
- Initialize
NFCTagReaderSessionwith polling options and assign a delegate. - Call
session.begin(). - Once a card is detected, the session invokes the corresponding delegate method.
- Implement the reading logic inside
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]).- Call
requestServiceinside this method. - Verify the service response, then call
readWithoutEncryption.
- Call
NFCTagReaderSession
To enable a class to read NFC, you must conform to NFCTagReaderSessionDelegate:
public protocol NFCTagReaderSessionDelegate : NSObjectProtocol {
// 當 session 可以開始讀取 NFC Tag 時呼叫
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession)
// 當呼叫 session invalidate 時呼叫
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error)
// 當 session 偵測到 tag 時呼叫
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag])
}
On Apple Developer, you can see all available tag types, including popular options like MiFare and FeliCa.
Block List
The block list specifies which blocks within a service you want to read. readWithoutEncryption requires a blockList parameter. For the definition of each bit in blockList, refer to the documentation. In CoreNFC, you can write it like this:
let blockList = (0..<UInt8(10)).map { Data([0x80, $0]) }
Here, 10 means requesting 10 blocks.
FeliCa Reader
To read FeliCa data, you must first know the corresponding service code—such as the service code for ride history or card balance. On this website (Japanese), we can find:
When System Code is 0003:
- Service code:
008B(1 block) - Card type and balance - Service code:
090F(20 blocks) - Ride history (20 entries) - Service code:
108F(3 blocks) - Transfer history (3 entries)
For me, ride history and balance are the most interesting pieces of data. Let’s inspect what information is stored inside these blocks:
One block is 16 bytes, and each byte stores corresponding data. Based on the documentation from that site, here are several key fields (numbers indicate byte offsets, and parentheses denote length in bytes):
- 0 (1 byte): Device type
- 1 (1 byte): Usage type (fare adjustment, new purchase, auto-reload, etc.)
- 2 (1 byte): Payment method (credit card, mobile, etc.)
- 3 (1 byte): Entry/exit type (entry, exit, etc.)
- 4–5 (2 bytes): Date (Year 7 bits, Month 4 bits, Day 5 bits). The year uses 2000 as the base, so 19 represents 2019.
- 6–9 (4 bytes): Entry/exit station codes, vending machine info
- A–B (2 bytes): Balance (Little Endian)
- C: Unknown
- F: Region code (Kanto private railways, Chubu private railways, Okinawa private railways, etc.)
With this knowledge, we can parse the information inside a for data in dataList loop! For example, parsing the entry/exit date:
let year = data[4] >> 1 // 不知道為什麼要右移一個才是正確的
let month = UInt(bytes: data[4...5]) >> 5 & 0b1111 // 取得 month 的 bit
let date = data[5] & 0b11111 // 取得 date 的 bit
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.date(from: "20\(year)-\(month)-\(date)")!
Demo
The source code is available on GitHub.
Wrap-up
Although I successfully read the data, some values weren’t quite what I expected. For instance, I searched extensively online for station codes but still couldn’t find a complete mapping, so I’m not certain if the parsed station codes are accurate.
Additionally, while up to 20 history entries can theoretically be retrieved, reading that many caused failures on certain cards; a safer limit is 10 (still testing this).
Looking at the records on my commuter pass, it also seems that not every tap in/out is logged. If your goal is to log every single journey for visualization or analytics, relying solely on card data might fall short.
Regarding FeliCa reading on iOS, a Japanese developer has built a very handy library called TRETJapanNFCReader. If you’re looking for a ready-to-use solution, definitely check it out. Besides SUICA, it supports many other types of IC cards and can even read driver’s licenses.
Ever since CoreNFC was introduced, I wanted to try reading FeliCa data. However, there weren’t many Chinese resources available. Japanese implementations were plentiful, but terms like service codes and block lists were still puzzling at first. So I spent a day reading through the FeliCa specs and built this implementation.
Also, type casting in Swift can be somewhat cumbersome—such as converting Data to UInt. I found this snippet online, and while I didn’t fully understand the mechanics behind it XD, it allowed me to convert data using UInt(bytes: Data):
import Foundation
extension FixedWidthInteger {
init(bytes: UInt8...) {
self.init(bytes: bytes)
}
init<T: DataProtocol>(bytes: T) {
let count = bytes.count - 1
self = bytes.enumerated().reduce(into: 0) { (result, item) in
result += Self(item.element) << (8 * (count - item.offset))
}
}
}
I haven’t been learning iOS development and Swift for very long, so I’m likely unfamiliar with idiomatic patterns and conventions, making the code a bit unpolished.
Related Posts
- When a Measure Becomes a Target: From the Window Tax to Pull Request Counts I once wrote a script to tally how many PRs I contributed in a quarter, how many reviews I left, and how many tickets I closed, hoping to use numbers to prove my output to my manager. My manager simply remarked that performance isn't just about output. Years later, I finally understood—when a measure becomes a target, it ceases to be a good measure. From the British window tax and the Hanoi rat bounty to evaluating developers by PR counts today, the underlying mechanism is exactly the same.
- Using Cloudflare Images for Image Storage and Transformation Putting an image on a webpage is the simplest task in frontend development. But doing it properly—including resizing, generating multiple formats, and withstanding heavy traffic—is actually an entire end-to-end solution. Eventually, I offloaded everything to Cloudflare Images, keeping only a single original image.
- Stop Using AWS Access Keys Access Keys are an easily overlooked security risk in AWS. By pairing OIDC with IAM Roles, GitHub Actions can securely operate AWS resources without storing any secrets.
- Database Primary Keys: AUTO_INCREMENT, UUID, and UUIDv7 Backend developers often face the choice of primary keys: should you use auto-increment or UUID? What about collisions? How does UUIDv7 compare to created_at + index in performance? Here are the design decisions and benchmark results from testing 20 million rows.