Reference
QR, links and NFC
A request is a string. A QR code, a web link and an NFC record are three ways to hand that same string to someone, and none of them changes what it says.
One string, three carriers
// One request, three carriers: QR text, a link, and an NFC NDEF record.
import { encode } from '@payvol/core';
import {
qrPayload,
linkPayload,
normalizeToUri,
ndefUriRecord,
parseNdefUriRecord,
} from '@payvol/qr';
const request = {
version: '0.1',
type: 'transfer',
recipient: 'payee::1220aabbcc',
instrument: 'DSO::1220be58/Amulet',
amount: '1.00',
};
const uri = encode(request);
console.log('uri ', uri);
console.log('qr payload ', qrPayload(request) === uri, '(the same string; QR carries the URI)');
console.log('web link ', linkPayload(request, { scheme: 'web+canton' }).slice(0, 24), '…');
console.log(
'normalised ',
normalizeToUri(linkPayload(request, { scheme: 'web+canton' })) === uri,
);
const record = ndefUriRecord(request);
console.log('ndef bytes ', record.length);
console.log('ndef header ', '0x' + record[0].toString(16));
console.log('ndef reads ', parseNdefUriRecord(record) === uri);
// The reader refuses what no writer produces.
try {
parseNdefUriRecord(new Uint8Array([...record, 0x00]));
} catch (error) {
console.log('trailing byte', error.code, '-', error.message.slice(0, 44));
} uri canton:payee%3A%3A1220aabbcc?instrument=DSO%3A%3A1220be58%2FAmulet&amount=1.00
qr payload true (the same string; QR carries the URI)
web link web+canton:payee%3A%3A12 …
normalised true
ndef bytes 83
ndef header 0xd1
ndef reads true
trailing byte MALFORMED - NDEF: 1 trailing byte(s) after the record
Output from @payvol/core version 0.4.0 and its sibling
packages, installed from npm, on 2026-10-18.
The QR payload is the URI: there is no separate QR encoding, which is why the sample
can assert equality rather than describe a transformation. The
web+canton link exists for contexts that will not follow a custom scheme, and
normalizeToUri turns it back into the canonical form.
What this package does not do
- It does not draw a QR image. It gives you the string; rendering is your renderer's decision, including error-correction level and quiet zone.
- It does not write an NFC tag. It gives you the NDEF record bytes. Getting them onto hardware is the host application's job, and no physical tag has been written in any test in this repository.
- It does not carry the draft profile. These carriers are defined for the
shipped profile. A
0.2-draftpayload is JSON, and putting it in a QR is a host decision this package does not make for you.
NDEF, and what the reader refuses
The record is a single well-known URI record with both message flags set. The reader is
stricter than a permissive parser on purpose: a record with the message flags unset, trailing
bytes after the payload, control characters inside the URI, or invalid UTF-8 is refused with a
named MALFORMED error rather than half read. The sample above shows the trailing
byte case.
Size
The URI has a transport bound, and encode enforces it rather than leaving you to
discover it at decode time. A request whose fields are large enough to exceed it is valid JSON
and not carryable as a URI, and those are answered separately: JSON validity and URI
portability are different questions.