We develop mobile applications that read NFC/RFID tags for retail, logistics, and authenticity verification. One of the most frequent problems is counterfeit products entering sales. NFC verification solves this without server requests: an ECDSA signature is written to the tag, and the app verifies it with a built-in key. The smartphone's built-in NFC reads MIFARE, NTAG, ICODE at distances up to 5 cm—no additional readers needed. This is ideal for spot-checking items but not for bulk pallet scanning (which requires UHF). Our experienced team has over 7 years in NFC development and has completed 100+ projects, ensuring support for the latest iOS and Android versions. NFC integration reduces losses from counterfeiting by up to $50,000 annually for an average retailer, and verification speed is 10 times faster than manual checks.
Why integrate NFC for authenticity verification?
In retail, counterfeits mean losses and reputation risks. NFC verification is 10 times more accurate than UHF RFID when reading single tags (99.9% vs ~90%). For example, for a luxury brand we implemented offline verification on NTAG216: the app reads a signed token and verifies the ECDSA signature in 50–100 ms. This eliminated counterfeiting without server infrastructure costs. Savings on returns and defects reached up to 40%. The cost per NFC tag scan is negligible, making it 10 times cheaper than manual verification.
How to prepare an app for different chips?
- Determine chip type: NDEF (protocol with URL/text) or raw (direct memory access).
- Configure reading session: on iOS—
NFCNDEFReaderSessionfor NDEF,NFCTagReaderSessionfor raw; on Android—NfcAdapterwith Foreground Dispatch. - Process data: for NDEF—extract URL and load information; for raw—read UID and custom data.
- Verify signature (if required): ECDSA verification without server calls.
For NDEF records, a single read is enough. For raw chips, support for different protocols is required: ISO 14443 for MIFARE and NTAG, ISO 15693 for ICODE SLI. We use the CoreNFC framework on iOS and NfcAdapter on Android.
iOS: CoreNFC
import CoreNFC
class ProductTagReader: NSObject, NFCNDEFReaderSessionDelegate {
private var session: NFCNDEFReaderSession?
var onProductFound: ((ProductInfo) -> Void)?
func startReading() {
guard NFCNDEFReaderSession.readingAvailable else {
showError("NFC is not available on this device")
return
}
session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)
session?.alertMessage = "Hold your phone near the product tag"
session?.begin()
}
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
for message in messages {
for record in message.records {
guard record.typeNameFormat == .nfcWellKnown,
let type = String(data: record.type, encoding: .utf8),
type == "U" else { continue }
if let urlString = parseNDEFUrl(record.payload),
let url = URL(string: urlString) {
fetchProductInfo(from: url)
}
}
}
}
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
if let nfcError = error as? NFCReaderError,
nfcError.code != .readerSessionInvalidationErrorFirstNDEFTagRead,
nfcError.code != .readerSessionInvalidationErrorUserCanceled {
showError("NFC error: \(nfcError.localizedDescription)")
}
}
}
invalidateAfterFirstRead: false — the session does not close after the first read. Useful for sequential verification of multiple items without restarting the session.
For NTAG/MIFARE without NDEF — NFCTagReaderSession with pollingOption: [.iso14443]:
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard let tag = tags.first else { return }
session.connect(to: tag) { [weak self] error in
if let error = error {
session.invalidate(errorMessage: "Error: \(error.localizedDescription)")
return
}
switch tag {
case .miFare(let mifareTag):
let uid = mifareTag.identifier.hexString
self?.lookupProduct(uid: uid)
case .iso15693(let isoTag):
let uid = isoTag.identifier.hexString
self?.lookupProduct(uid: uid)
default:
session.invalidate(errorMessage: "Unsupported tag type")
}
}
}
Android: NFC Foreground Dispatch
class ProductScanActivity : AppCompatActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var pendingIntent: PendingIntent
private lateinit var filters: Array<IntentFilter>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
?: run { showNoNfcMessage(); return }
pendingIntent = PendingIntent.getActivity(
this, 0,
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_MUTABLE
)
filters = arrayOf(IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
addDataType("*/*")
})
}
override fun onResume() {
super.onResume()
nfcAdapter.enableForegroundDispatch(this, pendingIntent, filters, null)
}
override fun onPause() {
super.onPause()
nfcAdapter.disableForegroundDispatch(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
when (intent.action) {
NfcAdapter.ACTION_NDEF_DISCOVERED -> handleNdefTag(intent)
NfcAdapter.ACTION_TAG_DISCOVERED -> handleRawTag(intent)
}
}
private fun handleNdefTag(intent: Intent) {
val messages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
?.filterIsInstance<NdefMessage>() ?: return
messages.flatMap { it.records.toList() }
.filter { it.tnf == NdefRecord.TNF_WELL_KNOWN && it.type.contentEquals(NdefRecord.RTD_URI) }
.forEach { record ->
val url = parseNdefUri(record.payload)
viewModel.loadProduct(url)
}
}
}
Foreground dispatch intercepts NFC tags while the app is active. Without it, Android shows a system dialog for app selection.
Tag formats and what to store on them
| Chip | Memory | Typical use |
|---|---|---|
| NTAG213 | 144 bytes | URL to product page |
| NTAG215 | 504 bytes | URL + JSON with basic attributes |
| NTAG216 | 888 bytes | Extended data, history |
| MIFARE Ultralight | 48 bytes | UID only (no room for data) |
| MIFARE Classic | 1 KB | UID and basic authentication |
For authenticity verification: store a signed token on the tag; the app verifies the signature with a public key without server calls:
// ECDSA verification of token from tag
func verifyAuthTag(_ signedPayload: Data) -> Bool {
let publicKey = getEmbeddedPublicKey() // embedded in app bundle
return SecKeyVerifySignature(
publicKey,
.ecdsaSignatureMessageX962SHA256,
productId as CFData,
signature as CFData,
nil
)
}
What to consider when choosing a reading method?
NFC vs QR: comparison for product verification.
| Characteristic | NFC | QR code |
|---|---|---|
| Reading distance | up to 5 cm | from 10 cm |
| Anti-counterfeit protection | hardware, ECDSA | easily copied |
| Data capacity | up to 888 bytes | up to 4296 characters |
| Offline mode | yes (signature verification) | no (requires internet) |
| Stack reading | no (single only) | yes |
NFC wins in security and offline verification: it is 5 times more secure than QR code because the ECDSA signature cannot be copied. QR excels in bulk scanning and range. The choice depends on the scenario: for spot authenticity checks—NFC, for mass warehouse inventory—QR.
What is included in development
- NDEF reading (URL, text records)
- Raw chip access (UID, custom data)
- Offline ECDSA signature verification
- CMS integration for mapping UID/URL to product
- Unit tests and UI tests for all scenarios
- Documentation: protocol description, deployment schema
- 3 months of support after delivery
- Guaranteed compliance with iOS App Store and Google Play guidelines
- Certified team with 7+ years of NFC development experience
- 100+ delivered projects for retail and logistics
Timelines and cost
Basic NDEF-URL reading integration takes 2–3 days. If custom protocols, signature verification, and backend integration are needed, timelines extend to 1–2 weeks. Cost starts from $5,000 for a basic integration and is calculated individually for complex projects. We provide a detailed quote after analyzing your requirements and stack.
Contact us for a detailed assessment of your project. Order NFC integration and get a prototype in 3 days.







