Reference
Reason codes
Every refusal names one of these. They are stable across a profile version, so an implementation in another language can branch on them and a conformance vector can pin them.
The codes
| Code | What it means |
|---|---|
MISSING_RECIPIENT | A transfer with no recipient. Also what a URI with no path decodes to. |
MISSING_INSTRUMENT | A transfer that does not say what is being asked for. |
MISSING_URL | A dynamic request with no endpoint. |
BAD_AMOUNT | An amount that is not a clean decimal string. |
BAD_URL_SCHEME | A dynamic url that does not use https. |
EXPIRED | Evaluated after the request deadline. |
UNKNOWN_TYPE | Neither transfer nor transaction. |
UNKNOWN_VERSION | A profile this implementation does not implement. |
BAD_SIGNATURE | A signature present but not standard base64. Shape only. |
DOMAIN_MISMATCH | The artifact’s own url host is absent from its own domains list. |
INPUT_TOO_LARGE | Past the transport bound, on the way in or on the way out. |
MALFORMED | Structurally wrong: bad grammar, bad percent-encoding, an unreadable date, invalid Unicode. |
What triggers each
Run against the published packages, with one input per code:
// Every reason code the packages can return, and one input that triggers it.
import { decode, validate, encode, PayvolReasonCode } from '@payvol/core';
const base = {
version: '0.1',
type: 'transfer',
recipient: 'payee::1220aabbcc',
instrument: 'DSO::1220be58/Amulet',
};
const show = (label, run) => {
try {
const r = run();
console.log(label.padEnd(26), r && r.valid === false ? r.code : 'accepted');
} catch (error) {
console.log(label.padEnd(26), error.code ?? error.constructor.name);
}
};
console.log('codes in the package:', Object.keys(PayvolReasonCode).length);
show('no recipient', () => validate({ ...base, recipient: undefined }));
show('no instrument', () => validate({ ...base, instrument: undefined }));
show('bad amount', () => validate({ ...base, amount: '12.5.0' }));
show('hour 24 is not a time', () => validate({ ...base, expiry: '2027-01-01T24:00:00Z' }));
show('url without a host', () =>
validate({ version: '0.1', type: 'transaction', url: 'https://' }),
);
show('unknown type', () => validate({ ...base, type: 'refund' }));
show('bad uri grammar', () => decode('canton:'));
show('over the transport bound', () => encode({ ...base, message: '漢'.repeat(1000) })); codes in the package: 12
no recipient MISSING_RECIPIENT
no instrument MISSING_INSTRUMENT
bad amount BAD_AMOUNT
hour 24 is not a time MALFORMED
url without a host MALFORMED
unknown type UNKNOWN_TYPE
bad uri grammar MISSING_RECIPIENT
over the transport bound INPUT_TOO_LARGE
Output from @payvol/core version 0.4.0 and its sibling
packages, installed from npm, on 2026-10-18.
Two lines in that output are worth reading twice. decode('canton:') answers
MISSING_RECIPIENT rather than MALFORMED: the grammar parsed, and what
was missing was the recipient. And an over-long request fails at encode with
INPUT_TOO_LARGE, not at decode: a request can validate and still not fit the URI
transport, which are separate questions with separate answers.
Why they are stable
A reason code is part of the artifact contract, not an implementation detail. Changing what a code means, or which one an input produces, changes what a conforming implementation must do, so it is a profile change rather than a patch. That is why the codes are enumerated in the package and pinned by vectors instead of being strings assembled at the point of failure.