NFC Encoding Guide
Encoding a TAG IT tag means personalising an NXP NTAG 424 DNA chip for SUN/SDM, so that every physical tap emits a fresh, cryptographically authenticated URL. That is done with a PC/SC reader and low-level APDU tooling. There is no TAG IT encoding SDK, no encoding API and no hosted encoding service — this page documents the chip mechanism that is real, and says plainly where the tooling stops.
Earlier revisions of this page showed new TagIt({ apiKey }) together with tagit.encoding.*, tagit.chips.*, tagit.crypto.* and tagit.alerts.* calls, plus /t/{TAG_ID} verification URLs. None of those exist. @tagit/sdk is not published to npm — the registry returns 404, so npm install @tagit/sdk fails — and the in-development package covers agent identity, reputation and validation only; it has no encoding, chip or key-management surface. The live system described below runs on Base Sepolia testnet (chain ID 84532) against unaudited contracts. Treat it as an evaluation environment, not a basis for a custody or settlement decision.
What exists today
Everything marked Live can be exercised right now. Everything marked Does not exist has no package, endpoint or method behind it, and nothing on this page pretends otherwise.
| What you want to do | How, today | Status |
|---|---|---|
| Understand what a 424 DNA tap emits and why it is trustworthy | This page plus NXP application note AN12196 | Documented |
| Verify a physical tap server-side | GET verify.tagit.network/api/verify?picc=&cmac= |
Live — needs a real tap |
| Fetch a Digital Product Passport credential for a tap | GET verify.tagit.network/api/dpp/01/{gtin}/21/{serial}?picc=&cmac= |
Live — needs a real tap |
| Show a human-readable verification page | verify.tagit.network/asset/{tokenId}, /tag/{uid}, /01/{gtin}/21/{serial} |
Live |
| Read an asset's owner and lifecycle state with no tag at all | getAsset on TAGITCore via viem |
Live |
| Personalise a blank NTAG 424 DNA chip | Internal PC/SC tooling driving an ACS ACR1252U reader | Internal only — not distributed |
| Bind a chip to an asset on-chain, activate or flag it | On-chain writes, gated by a BIDGES capability badge | Not open to the public |
| Encoding sessions, batch-encoding API, key-management API, chip registry API, fraud-alert webhooks, certified encoding stations | — | Do not exist |
How an NTAG 424 DNA tap works
This part is genuine hardware behaviour, defined by NXP, and it is what makes a TAG IT tag hard to clone. The chip stores an NDEF URL, and in SDM (Secure Dynamic Messaging, marketed as SUN — Secure Unique NFC) mode it rewrites two regions of that URL on every single read, using AES keys that were written into the chip during personalisation and can never be read back out.
A tap therefore produces a URL like this, with different values each time:
https://verify.tagit.network/01/{GTIN}/21/{SERIAL}?picc=<32 hex>&cmac=<16 hex>
picc 16 bytes, AES-128-CBC encrypted (zero IV) under the chip's SDMMetaRead key.
Plaintext = 0xC7 tag byte | 7-byte UID | 3-byte read counter (little-endian)
cmac 8 bytes, a truncated AES-CMAC computed under a per-tap session key that is
itself derived from the chip's SDMFileRead key, its UID and the counter
Three consequences follow, and they are the whole security argument:
- The UID is not in the clear. It is inside the encrypted
piccblob, so a URL cannot be hand-crafted for a chosen UID without the key. - The signature cannot be produced off-chip. The AES keys live in the chip's protected key store and in the verifier's server environment — nowhere else. A phone, a browser or a copied URL cannot compute a fresh valid
cmac. - Every read is distinguishable. The chip increments a 24-bit read counter (0–16,777,215) on each tap and folds it into the session key, so a replayed URL is a replay of one specific past tap, and is detectable as such.
Because the SDM keys are not on the reading device, nothing client-side can decide whether a tap is genuine. The mobile app forwards picc and cmac to the verifier and renders the answer. Any “offline verification” claim for this chip family would be false.
What is written to the chip
Personalisation writes a single NDEF URI record into the chip's NDEF file (file 2, 256 bytes of capacity on the 424 DNA — the chip's 416 bytes of user memory are split across the 32-byte CC file, this 256-byte NDEF file and a 128-byte proprietary file), with the picc and cmac parameters present as zero-filled placeholders. It then enables SDM and tells the chip the byte offsets of those two placeholders, so the chip knows where to inject the encrypted PICC data and the MAC at read time.
File 2 layout (NFC Forum Type 4 Tag NDEF file)
bytes 0-1 NLEN, big-endian length of the NDEF message
byte 2 0xD1 record header: MB=1, ME=1, SR=1, TNF=0x01 (well-known)
byte 3 0x01 type length
byte 4 payload length
byte 5 'U' record type: URI
byte 6 0x04 URI identifier code: "https://"
bytes 7-.. URI body:
verify.tagit.network/...?picc=00000000000000000000000000000000
&cmac=0000000000000000
The offsets of the two zero runs are passed to the chip as PICCDataOffset and
SDMMACOffset. The chip overwrites exactly those bytes on every read.
Nothing else is written. There is no metadata record, no signature record, no product JSON on the chip — the URL is the entire payload, and all product data is resolved from the chain and from off-chain metadata after the tap is verified.
Key layout
The 424 DNA has five application keys. TAG IT personalisation uses three of them:
| Key | Role | Value | Held by |
|---|---|---|---|
0 |
Application master (authentication) | Left at the factory default in current tooling | — |
1 |
SDMFileRead — computes the tap CMAC | Diversified per chip from the master secret and the chip's UID | Chip; re-derived by the verifier |
2 |
SDMMetaRead — encrypts the PICC data | The master secret itself, identical across the fleet | Chip; verifier environment |
Key 2 cannot be diversified per chip: the verifier has to decrypt picc before it knows which UID it is dealing with, so the decryption key must be fleet-wide. Key 1 is diversified, so compromising one chip's CMAC key does not forge taps for any other chip.
Key 0 is deliberately left at its factory value so that personalisation is re-runnable and a chip can never be bricked during development. The trade-off is real and should be stated rather than glossed: anyone with physical access to a tag and a reader can re-run personalisation against it. Forging a valid tap for the TAG IT verifier still requires the master secret, which is not on the chip in recoverable form — but permanent configuration locking is not enabled today. Do not describe these tags as tamper-locked.
URL forms that resolve
Every route below is relative to the public verification host, verify.tagit.network. You can check an asset on-chain right now without a chip or a wallet.
Only the routes below exist. In particular, verify.tagit.network/t/{TAG_ID} and verify.tagit.network/{tagId} — both previously documented on this page — return 404. There is no 12-character alphanumeric tag identifier anywhere in the system; the on-chain tag identifier is keccak256 of the chip's raw 7-byte UID.
| Route | Returns |
|---|---|
/01/{gtin}/21/{serial}?picc=&cmac= |
GS1 Digital Link carrier — the form new chips are personalised with. Human-readable passport page for a verified tap. |
/api/dpp/01/{gtin}/21/{serial}?picc=&cmac= |
The same passport as a W3C Verifiable Credential (VCDM 2.0 JSON-LD, UNTP Digital Product Passport). Unsigned in v1 — integrity comes from the on-chain anchor carried in evidence, and the response says so in an x-dpp-proof header. |
/api/verify?picc=&cmac= |
Flat JSON verdict for a tap. Used by the mobile scanner. |
/asset/{tokenId} |
Human-readable page for a digital twin, addressed by token ID. No tap required, so it proves the record exists — not that you are holding the product. |
/tag/{uid} |
Human-readable page addressed by chip UID. |
/sun?picc=&cmac= |
Legacy carrier, kept working for chips personalised before the GS1 form. Do not use it for new chips. |
The GS1 GTIN and serial in the path are the standards-compliant identity layer. They are rendered alongside the result but they do not drive resolution — the UID recovered from the encrypted picc blob is the trust anchor, because it is the only part of the URL an attacker cannot choose.
Verifying a tap
The verifier runs this pipeline. Each step can fail, and the failure is reported rather than swallowed:
- Reject the request unless
piccis 32 hex characters andcmacis 16. - Decrypt
piccwith AES-128-CBC (zero IV) under the SDMMetaRead key. - Check the first plaintext byte is
0xC7, the PICC data tag meaning “UID and counter present”. Read out the 7-byte UID and the 3-byte little-endian read counter. - Derive the per-chip SDMFileRead key from the master secret and that UID, then derive the per-tap session key from the UID and counter.
- Recompute the truncated AES-CMAC and compare it to
cmacin constant time. A mismatch means counterfeit, and the request stops here. - Compute
keccak256(UID)and callgetTokenByTagon TAGITCore. A result of0means the chip is genuine but not yet bound to any asset. - Call
getAsset(tokenId)for the owner, timestamp and lifecycle state, and render the verdict.
The HTTP call is a plain GET. The picc and cmac values below are placeholders: only a genuine chip on a genuine tap can produce a pair that verifies, so this command is runnable only with values captured from real hardware.
curl "https://verify.tagit.network/api/verify?picc=<32 hex from the tap>&cmac=<16 hex from the tap>"
The response is a flat JSON object. Its fields:
| Field | Meaning |
|---|---|
verified | Boolean. True only when the tap's crypto checks out and the asset is in lifecycle state 1–4. |
bound | Whether the chip resolves to an on-chain digital twin. |
uid | The chip UID recovered from the encrypted blob, uppercase hex. |
tapCounter | The chip's read counter for this tap. |
asset | Present when bound: tokenId, stateCode, lifecycleState, owner, timestamp, and product fields (name, image, brand, sku, origin, msrp) from off-chain metadata. |
metadataHash | On-chain metadata anchor, or null. |
chain | { id: 84532, name: "Base Sepolia" }. |
reason / error | Why a tap failed. Missing parameters return 400; an unconfigured verifier returns 503; a chain read failure returns 502. |
The chip's counter is genuine and the verifier returns it as tapCounter, so a client that stores the highest counter it has seen per UID can detect a replayed URL itself. The hosted verifier does not currently persist a last-seen counter, does not reject a stale one, and raises no fraud alerts — there is no counter API and no alerting endpoint. If replay resistance matters to your integration, do that check on your side.
Reading the twin without a tap
Verifying presence needs the physical chip. Reading the asset record does not, and needs no credential of any kind. This was executed on 26 July 2026 and returns the values shown:
import { createPublicClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';
const client = createPublicClient({ chain: baseSepolia, transport: http() });
const TAGITCoreABI = [{
name: 'getAsset', type: 'function', stateMutability: 'view',
inputs: [{ name: 'tokenId', type: 'uint256' }],
outputs: [
{ name: 'assetOwner', type: 'address' },
{ name: 'timestamp', type: 'uint64' },
{ name: 'state', type: 'uint8' },
{ name: 'flags', type: 'uint8' },
{ name: 'reserved', type: 'uint16' },
],
}];
const [owner, timestamp, state] = await client.readContract({
address: '0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D',
abi: TAGITCoreABI,
functionName: 'getAsset',
args: [50n],
});
console.log({ owner, timestamp, state });
// → { owner: '0x458B4d0c3a55006965Fd13D6af7B8509De51Cb3D',
// timestamp: 1784246184n, state: 4 } // 4 = CLAIMED
Lifecycle states
state and the stateCode field in the verifier response are the same integer. Branch on the number, not on a label:
| Code | State | Meaning |
|---|---|---|
0 | NONE | Token does not exist |
1 | MINTED | Digital twin created, no chip bound yet |
2 | BOUND | NFC chip cryptographically bound |
3 | ACTIVATED | QA passed, ready for distribution |
4 | CLAIMED | Owned by an end consumer |
5 | FLAGGED | Reported lost, stolen or recalled |
6 | RECYCLED | End of life, terminal |
Programming a chip
Personalisation is a sequence of DESFire commands wrapped in ISO 7816 APDUs — authenticate EV2 First, ChangeKey for keys 1 and 2, WriteData for the NDEF file, ChangeFileSettings to enable SDM with the two offsets. It requires physical contact with a contactless reader, so it is inherently a local operation; no web API can perform it.
Hardware
- NXP NTAG 424 DNA — the only chip TAG IT supports. NTAG 213/215/216 cannot do SDM and are not usable for this flow; see Chip Selection.
- ACS ACR1252U — the USB PC/SC reader the internal tooling is written and tested against.
- A host with PC/SC — macOS and Linux both expose the reader natively. Browsers cannot talk to PC/SC at all, which is why a local daemon exists.
Internal tooling — not publicly distributed
The personalisation implementation lives in the private tagit-nfc-bridge repository. It is a local daemon that binds to 127.0.0.1, exposes the reader to the admin console over an authenticated WebSocket, and performs the APDU sequence against whichever chip is on the antenna. It is not published to npm and not available for download, so the following is documentation of an internal interface, not something an external reader can run:
{
"type": "personalize-sdm",
"id": "<request id>",
"baseUrl": "https://verify.tagit.network/01/<GTIN>/21/<SERIAL>"
}
The daemon appends the ?picc=…&cmac=… placeholders itself, computes the offsets and returns the exact URL template it wrote. The master secret is read from local configuration and never crosses the WebSocket.
If you need chips personalised for a pilot, that is currently done by the TAG IT team rather than self-served. Say so to anyone planning an integration timeline — there is no self-service encoding path today.
What does not exist
To be unambiguous, so that none of this reappears: there is no tagit.encoding, tagit.chips, tagit.crypto, tagit.alerts, tagit.nfc, tagit.tags, tagit.products or tagit.security namespace in any TAG IT package. There is no BatchEncoder, no encoding session, no createBatch, no generateKeys, no post-encoding verify call, no key-management service, no encoding permission on any API key — there are no API keys — no certified encoding station product, no fraud-alert webhook, no counter-anomaly endpoint, no sandbox host and no mainnet deployment. If you hold code written against any of those, it never ran. Replace tap verification with /api/verify and asset lookups with the direct on-chain read.
Troubleshooting
A tap does not verify
bad picc hex/bad cmac hex— the parameters were truncated or re-encoded in transit.piccmust be exactly 32 hex characters,cmacexactly 16. Link shorteners and analytics redirects that rewrite query strings will break this.cmac mismatch— either the URL was tampered with, or the chip was personalised with a different master secret than the deployment being queried. This is the expected result for a copied or hand-crafted URL, which is the point.unexpected PICC tag— the chip's SDM configuration is not mirroring UID plus counter. Re-run personalisation.- HTTP
503, verifier not configured — the deployment has no SDM master key set. This is an environment problem, not a chip problem. - Authentic but unbound — the crypto passed and
boundisfalse. The chip is real; nobody has bound it to an asset on-chain yet. This is the normal state of a freshly personalised chip.
The reader is not responding
- Bridge not connected in the console — in order, check the origin allowlist, then the per-origin token pasted into the console's NFC Reader settings, then a stale PC/SC handle (unplug and replug the reader, or restart the daemon).
- Chip not detected — the 424 DNA is a 13.56 MHz ISO 14443-A part. Metal or liquid directly behind the antenna will kill the read; reposition before assuming the chip is dead.
- Personalisation fails partway — it is safe to re-run. The flow is written to be idempotent and leaves key 0 at its factory value precisely so a half-finished chip can be recovered rather than discarded.
Join our Discord community, or see Chip Selection for the hardware decision and Contract Addresses for the deployment this page reads from.