Features Solutions Technology Tokenomics Docs About Launch App
Docs / NFC Integration / Mobile SDK

NFC Mobile SDK

There is no TAG IT NFC package to install on iOS or Android. This page documents what a native app actually does with a TAG IT chip: read the URL it emits using the platform NFC API, then verify that tap server-side.

No NFC SDK is published

There is no CocoaPod named TagItNFC, no Swift Package Manager package, no Maven or Gradle artifact network.tagit:nfc-sdk, and no class called TagItNFCScanner. The repository TAG-IT-NETWORK/tagit-nfc-ios does not exist either. Checked against the public registries on 2026-07-26: CocoaPods trunk and Maven Central both return HTTP 404 for those names, and so does npm for @tagit/sdk.

Everything below uses Apple's Core NFC and Android's NfcAdapter directly, with no TAG IT dependency — because there is nothing to depend on. That is not a workaround: a TAG IT chip emits a plain HTTPS URL, so the platform NFC stack is all a reader needs.

What exists today

TAG IT is deployed to the Base Sepolia testnet (chain ID 84532) and the contracts are unaudited. Nothing on this page should be treated as production infrastructure.

Component What it is Status
ORACULAR app First-party scanner app (Expo / React Native). Tap a chip, verify, view the asset. Internal builds only — not on the App Store or Google Play, and not distributed as a library
verify.tagit.network/api/verify Server-side verification of an NTAG 424 DNA SUN tap Live
verify.tagit.network/api/dpp/... Digital Product Passport (W3C Verifiable Credential) for a GS1 Digital Link tap Live
TAGITCore contract Direct on-chain read of asset owner, timestamp and lifecycle state Live on Base Sepolia
iOS / Android NFC SDK Installable CocoaPod, SPM package or Gradle artifact from TAG IT Does not exist

Device requirements

These are platform constraints, not TAG IT ones:

Note

iPhone XS and later read NDEF tags in the background with no app open or installed. Because a TAG IT chip carries an ordinary HTTPS URL, that background tap simply opens the verifier in Safari — which is the fallback path for every consumer who does not have your app.

Why the tap is verified server-side

TAG IT uses the NXP NTAG 424 DNA chip in SUN (Secure Unique NFC) mode. On every tap the chip rewrites its own NDEF URL and appends two query parameters:

A phone reading the tag receives ciphertext and a MAC and nothing else. It cannot decrypt the PICC or validate the CMAC on-device, because both require the SDM key — and shipping that key inside an app binary would let any attacker mint valid taps for every chip in the fleet. So the app forwards picc and cmac to the verifier, which holds the key server-side, checks the CMAC, rejects replays using the tap counter, and resolves the on-chain twin.

There is no offline verification, and there cannot be

Offline SUN verification is not a missing feature — it is cryptographically impossible under this design without putting the SDM key on the device. An app with no connectivity can show a cached earlier result, but it cannot establish that a fresh tap is genuine.

The tap → verify flow

  1. Read the NDEF URI record from the chip with the platform NFC API.
  2. Pull picc and cmac out of that URL's query string.
  3. GET https://verify.tagit.network/api/verify?picc=<32 hex>&cmac=<16 hex>
  4. Render the JSON response.

Chips are personalized to a SUN landing URL carrying those parameters, for example:

https://verify.tagit.network/sun?picc=<32 hex>&cmac=<16 hex>

You can exercise the endpoint right now. Parameters that are not a genuine tap return HTTP 200 with a structured rejection:

curl -s "https://verify.tagit.network/api/verify?picc=00000000000000000000000000000000&cmac=0000000000000000"
{
  "verified": false,
  "reason": "unexpected PICC tag 0xba",
  "chain": { "id": 84532, "name": "Base Sepolia" }
}

There is no API key and no registration step: the endpoint is public because the chip's CMAC is the credential. Producing "verified": true requires a physical chip — a valid picc/cmac pair cannot be synthesized without one.

Permissions & entitlements

iOS (Info.plist)

<key>NFCReaderUsageDescription</key>
<string>Used to verify the authenticity of your TAG IT-chipped items.</string>

You must also enable the Near Field Communication Tag Reading capability in your target's Signing & Capabilities tab. The com.apple.developer.nfc.readersession.iso7816.select-identifiers entitlement is only needed if you drive the chip with raw APDUs via NFCTagReaderSession; the SUN flow does not require it, because everything you need is already in the NDEF record.

Android (AndroidManifest.xml)

<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />

android:required="true" hides your app on Google Play from devices without NFC hardware. Set it to false and check NfcAdapter.getDefaultAdapter(context) != null at runtime if the rest of your app should still install there.

iOS — Core NFC

Read the URI record with NFCNDEFReaderSession. No third-party dependency is involved:

import CoreNFC

enum TapError: Error { case notASunURL }

final class TapReader: NSObject, NFCNDEFReaderSessionDelegate {

    private var session: NFCNDEFReaderSession?
    private var onURL: ((URL) -> Void)?

    func begin(onURL: @escaping (URL) -> Void) {
        guard NFCNDEFReaderSession.readingAvailable else { return }
        self.onURL = onURL

        let session = NFCNDEFReaderSession(
            delegate: self, queue: nil, invalidateAfterFirstRead: true
        )
        session.alertMessage = "Hold your iPhone near the TAG IT chip"
        session.begin()
        self.session = session
    }

    // A chip in SUN mode carries one URI record: the verifier URL, with a
    // picc/cmac pair the chip regenerated for this specific tap.
    func readerSession(_ session: NFCNDEFReaderSession,
                       didDetectNDEFs messages: [NFCNDEFMessage]) {
        for message in messages {
            for record in message.records {
                if let url = record.wellKnownTypeURIPayload() {
                    session.alertMessage = "Chip read"
                    onURL?(url)
                    return
                }
            }
        }
        session.invalidate(errorMessage: "No URL record on this chip")
    }

    // Also fires on user cancel and on the 60-second session timeout.
    func readerSession(_ session: NFCNDEFReaderSession,
                       didInvalidateWithError error: Error) {
        self.session = nil
    }
}

Then hand the URL to the verifier. This is the whole network integration — one GET, no headers beyond Accept:

// URLSession.data(for:) requires iOS 15+.
func verifyTap(_ tagURL: URL) async throws -> Data {
    guard
        let tapped = URLComponents(url: tagURL, resolvingAgainstBaseURL: false),
        let picc = tapped.queryItems?.first(where: { $0.name == "picc" })?.value,
        let cmac = tapped.queryItems?.first(where: { $0.name == "cmac" })?.value
    else { throw TapError.notASunURL }

    var endpoint = URLComponents(string: "https://verify.tagit.network/api/verify")!
    endpoint.queryItems = [
        URLQueryItem(name: "picc", value: picc),
        URLQueryItem(name: "cmac", value: cmac),
    ]

    var request = URLRequest(url: endpoint.url!)
    request.setValue("application/json", forHTTPHeaderField: "Accept")

    let (data, _) = try await URLSession.shared.data(for: request)
    return data   // decode into your own Codable type — see the fields below
}

Android — NfcAdapter

Reader mode keeps the tap inside your Activity instead of firing an intent, which is what you want behind a deliberate "scan" button:

import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.Ndef
import androidx.appcompat.app.AppCompatActivity

class ScanActivity : AppCompatActivity() {

    private val nfcAdapter: NfcAdapter? by lazy { NfcAdapter.getDefaultAdapter(this) }

    override fun onResume() {
        super.onResume()
        // The callback runs on a binder thread, not the main thread.
        nfcAdapter?.enableReaderMode(
            this,
            { tag -> readSunUrl(tag)?.let { url -> onTagUrl(url) } },
            NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
            null,
        )
    }

    override fun onPause() {
        super.onPause()
        nfcAdapter?.disableReaderMode(this)
    }

    /** NTAG 424 DNA in SUN mode exposes a single NDEF URI record. */
    private fun readSunUrl(tag: Tag): String? {
        val ndef = Ndef.get(tag) ?: return null            // not an NDEF tag
        // Cached at discovery — a plain read needs no connect().
        val message = ndef.cachedNdefMessage ?: return null
        return message.records.firstNotNullOfOrNull { it.toUri()?.toString() }
    }

    /** Your handler: send the URL to verifyTap() below, then render the result. */
    private fun onTagUrl(url: String) { /* ... */ }
}

The verification call, with no HTTP library beyond the JDK:

import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL

suspend fun verifyTap(tagUrl: String): String = withContext(Dispatchers.IO) {
    val tapped = Uri.parse(tagUrl)
    val picc = tapped.getQueryParameter("picc")
    val cmac = tapped.getQueryParameter("cmac")
    require(picc != null && cmac != null) { "Not a SUN URL: missing picc or cmac" }

    val endpoint = Uri.parse("https://verify.tagit.network/api/verify")
        .buildUpon()
        .appendQueryParameter("picc", picc)
        .appendQueryParameter("cmac", cmac)
        .build()
        .toString()

    val connection = URL(endpoint).openConnection() as HttpURLConnection
    connection.setRequestProperty("Accept", "application/json")
    try {
        connection.inputStream.bufferedReader().use { it.readText() }
    } finally {
        connection.disconnect()
    }
}

What the verifier returns

These are the fields the first-party ORACULAR app reads from a verification response (see src/services/sunVerify.ts in the tagit-mobile repository). Model your decoder on this set rather than assuming anything wider:

Field Type Meaning
verifiedbooleanWhether the CMAC checked out against a known chip
reasonstringWhy verification failed — present when verified is false
uidstringChip UID, recovered server-side by decrypting the PICC
tapCounternumberThe chip's monotonic tap counter, used for replay rejection
chain.id / chain.namenumber / stringChain the twin was resolved on — 84532 / Base Sepolia
asset.tokenIdstringToken ID of the on-chain digital twin
asset.stateCodenumberLifecycle state as a number — see the table below
asset.lifecycleStatestringThe same state as a name, e.g. CLAIMED
asset.ownerstringCurrent owner address
asset.timestampnumberUnix seconds of the last state change

Lifecycle states

asset.stateCode — and the state byte returned by the contract — map to the seven-state asset lifecycle:

Value State Meaning
0NONEToken does not exist
1MINTEDDigital twin created, no chip bound yet
2BOUNDNFC chip cryptographically linked to the twin
3ACTIVATEDPassed QA, ready for market
4CLAIMEDOwned by an end consumer
5FLAGGEDLost, stolen or under investigation
6RECYCLEDEnd of life, terminal

Reading asset state without the verifier

If you already hold a token ID — from a verified tap, a deep link or your own records — you can read its state straight from the contract with no TAG IT service in the path. TAGITCore is deployed on Base Sepolia at 0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D, and getAsset(uint256) is a plain eth_call, so URLSession or HttpURLConnection is enough — no wallet library required.

The calldata is the selector 0xeac8f5b8 followed by the token ID padded to 32 bytes. This request and its response were executed against Base Sepolia on 2026-07-26:

curl -s -X POST https://sepolia.base.org \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
        "to":"0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D",
        "data":"0xeac8f5b80000000000000000000000000000000000000000000000000000000000000032"
      },"latest"]}'
{"jsonrpc":"2.0","result":"0x000000000000000000000000458b4d0c3a55006965fd13d6af7b8509de51cb3d000000000000000000000000000000000000000000000000000000006a596fa8000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","id":1}

The return value is (address assetOwner, uint64 timestamp, uint8 state, uint8 flags, uint16 reserved), one 32-byte word each. Decoding the result above, for token ID 50:

Word Field Value
1assetOwner0x458B4d0c3a55006965Fd13D6af7B8509De51Cb3D
2timestamp0x6a596fa8 = 1784246184
3state4 — CLAIMED
4flags0
5reserved0

Note what this read does not tell you: that a physical chip was present. Only a server-verified tap establishes that. An on-chain read describes the state of the digital twin, and says nothing about the object in the customer's hand.

Seven of the eight state-changing functions — mint, bindTag, activate, claim, flag, resolve and recycle — are gated on a BIDGES capability badge, so an ordinary wallet cannot call them. The exception is transferAsset, the peer-to-peer resale path: it is owner-gated rather than capability-gated, so whoever currently owns a CLAIMED asset may call it. Plain ERC-721 transferFrom is blocked by the contract's _update() override, which makes transferAsset the only consumer transfer route.

None of that changes what the code on this page does. Reading is a bare eth_call; writing needs a signing wallet, which Core NFC and NfcAdapter do not provide. A scanner built from this page is a read-and-verify client.

React Native

The ORACULAR app is Expo + React Native and reads chips with the third-party react-native-nfc-manager package, then verifies through the same endpoint documented above. That path, including the crypto polyfills React Native needs before viem will run, is written up on the Mobile SDKs page.

Testing & troubleshooting

Physical devices only

NFC does not work in the iOS Simulator, the Android emulator or Expo Go. Nor can a tap be faked in software: a valid picc/cmac pair can only come from a chip, so end-to-end testing needs real hardware.

Not available yet

Listed explicitly so that nobody plans around them:

Testnet, unaudited

Contracts are deployed to Base Sepolia (84532) and have not completed a third-party audit. The token ID, address and responses shown here are testnet artifacts and may be reset. Do not build production or custody-bearing flows on them.

Need Help?

Deployed addresses are listed on the Contract Addresses page. For anything else, ask in our Discord community — if a capability you need is missing, tell us rather than assuming it is coming.

Edit this page on GitHub
Type to search documentation...