Skip to content
Payvol

Get started

Your first request

Ten minutes, no account, and nothing of ours running anywhere. By the end you will have a request, its identifier, and three ways to hand it to somebody.

Install

Two packages. @payvol/core encodes, decodes and validates; @payvol/qr turns a request into the forms a payer can actually receive.

npm i @payvol/core @payvol/qr

Both are on npm and install from the registry. Tested on Node 20. A party id and an instrument id are explained under Create a request, because a request that validates is not automatically one a wallet can pay.

Create a request

A request names who is being paid, in what, how much, and an optional reference that stays yours. Encoding it produces a canton: URI, and that URI is the whole artifact: there is no session, and nothing was stored anywhere.

import { encode } from '@payvol/core'

const uri = encode({
  version: '0.1',
  type: 'transfer',
  recipient: 'payvol-demo-recipient',
  instrument: 'DSO::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a/Amulet',
  amount: '1.5000000000',
  reference: 'PAYVOL-DEMO-2026-08-01',
})

What those two identifiers are. A Canton party id is name::fingerprint, where the fingerprint is 68 hex characters beginning 1220. You get yours from the validator or wallet that hosts the party: it is shown when the party is allocated, and any wallet connected to it will display it. An instrument id is admin/id, where admin is the party id of the registry that issues the asset and id is its symbol. The DevNet Canton Coin admin above comes from the network's own registry metadata endpoint, captured in this repository's fixtures rather than typed by hand.

A valid request is not a settleable one. validate() answers whether the artifact is the right shape. It returns warnings when the recipient is not a full party id, or the instrument names no registry admin, because those are well formed and unpayable. What settles a request is a wallet: the payer opens it, the wallet builds a Canton Token Standard transfer, and the payer signs it. You can try that at /app/pay.

That returns:

canton:payvol-demo-recipient?instrument=DSO%3A%3A1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a%2FAmulet&amount=1.5000000000&reference=PAYVOL-DEMO-2026-08-01

Seal it

The identifier is derived from the canonical bytes rather than assigned, so two independent implementations that agree on the profile produce the same identifier for the same request. Change one character and it changes completely.

import { decode, sha256, toCanonical } from '@payvol/core'

// the canonical bytes are what an identifier is derived FROM, never assigned
const canonical = toCanonical(decode(uri))
const identifier = sha256(canonical)

// change one character of the request and this changes completely

The digest is domain separated, so it cannot collide with a digest computed under another profile over the same bytes. SPEC.md in the repository says exactly how.

Deliver it

The same bytes become a QR code, a link, or an NFC tap. Nothing is installed on the payer side and no page has to stay up for the code to keep working.

import { qrPayload, linkPayload, ndefUriRecord } from '@payvol/qr'

// the same bytes, three ways to hand them over
const forQr = qrPayload(uri)
const forLink = linkPayload(uri, { scheme: 'web+canton' })
const forTap = ndefUriRecord(uri)

Read it back

A reader decodes the artifact and checks it against the profile. A request it cannot read is refused with a named reason rather than half understood.

import { decode, validate } from '@payvol/core'

const request = decode(scanned)
const result = validate(request)

// a refusal names its rule; it is never a bare false
result.valid   // false
result.code    // 'EXPIRED'

Profile 0.1 is pinned by 42 conformance vectors. Run them against your own implementation with npx @payvol/cli conformance, which needs nothing installed and no repository checkout.

What to do next