Skip to content
Payvol

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));
}
Output, captured from a real run
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-draft payload 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.