Skip to main content

Data Formats

This page documents the JSON payloads, ciphertext format, and API request/response schemas used by Veylent.

Anchor Payload (Signing Record)​

The signing record contains the anchor ID, document hash, and the signer's identity. It is serialized as compact JSON, encrypted with the key from the QR code, and stored on the API server. The composite hash (SHA-256 of this JSON) is stored on-chain. The same format is AnchorPayload in the desktop app (payload.rs) and SignerPayload in libs/shared/types.

{
"a": "0x0123456789abcdef0123456789abcdef",
"d": "0xa1b2c3...f0",
"s": {
"t": "individual",
"n": "John Doe",
"e": "john@example.com",
"c": "Acme Corp",
"p": "CTO",
"tr": "google",
"v": true
},
"ts": 1774288200,
"g": {
"la": 6.9271,
"ln": 79.8612
},
"salt": "e4f5a6...b7c8"
}

Field Reference​

FieldKeyTypeRequiredDescription
Anchor IDastringYesThe signature's anchor ID, 0x + 32 hex chars. Ties the record to its QR code.
Document hashdstringYes0x + SHA-256 hex of the saved file, through the end of this signer's layer
Signer infosobjectYesSigner identity
Signer types.tstringYes"individual" or "company"
Names.nstringYesSigner's display name
Emails.estringYesSigner's email address
Companys.cstringNoCompany name (omitted if empty)
Positions.pstringNoJob title (omitted if empty)
Trust anchors.trstringNoAuth provider: "email", "google", "microsoft"
Verifieds.vbooleanNoWhether identity was verified by the trust anchor
TimestamptsintegerYesUnix epoch seconds (UTC)
GeolocationgobjectNoGPS coordinates at signing time
Latitudeg.lanumberIf g presentDecimal degrees
Longitudeg.lnnumberIf g presentDecimal degrees
SaltsaltstringYes32 random bytes, hex-encoded (64 chars)

The tr and v fields are populated from the signer's JWT claims. They allow verifiers to display a trust badge indicating how the signer authenticated (e.g. "Authenticated via Google").

Short keys are used to keep the sealed record small.

Composite Hash​

The composite hash is computed by SHA-256 hashing the exact serialized JSON text:

composite_hash = "0x" + hex(SHA-256(json_bytes))

This hash is the only value about the record stored on the blockchain. It commits to the entire record -- any change to any field produces a different hash. Verifiers hash the decrypted text as received, so the JSON is never re-serialized before hashing.

Salt Purpose​

The 32-byte random salt ensures that:

  • Two identical signings produce different composite hashes
  • The composite hash cannot be brute-forced from known signer data
  • Each signature is cryptographically unique

Ciphertext Format​

The encrypted payload uses AES-128-GCM with a prepended nonce:

Ciphertext format

ComponentSizeDescription
Nonce12 bytesRandom, generated per encryption
Encrypted dataVariableAES-128-GCM ciphertext
Auth tag16 bytesGCM authentication tag (appended by AES-GCM)

The entire ciphertext (nonce + encrypted + tag) is base64url-encoded for storage and transmission.

Key​

  • Size: 16 bytes (128 bits)
  • Generation: CSPRNG (OsRng)
  • Encoding: Base64url without padding (22 characters)
  • Location: QR code URL fragment only

API Schemas​

POST /api/relay -- Submit Anchor​

Request:

{
"anchorId": "0x0123456789abcdef0123456789abcdef",
"compositeHash": "0xabc123...def789",
"previousAnchorId": "0x00000000000000000000000000000000",
"encryptedPayload": "<base64url-encoded ciphertext>"
}
FieldTypeValidationDescription
anchorIdstring0x + 32 hex charsThe signature's anchor ID, as printed in its QR code
compositeHashstring0x + 64 hex charsSHA-256 of the signing record
previousAnchorIdstring0x + 32 hex charsPrevious signer's anchor ID, or all zeros for the first signature
encryptedPayloadstringString, max 64,000 charsBase64url-encoded AES-128-GCM ciphertext

Response:

{
"anchorId": "0x0123456789abcdef0123456789abcdef",
"txHash": "0x123...789",
"blockNumber": 42
}

Returns 409 if the anchor ID is already in use, and 400 if the previous anchor ID is not on the blockchain.

GET /api/verify/:anchorId -- Verify Signature​

URL parameter: anchorId -- 0x + 32 hex chars

Response:

{
"anchorId": "0x0123456789abcdef0123456789abcdef",
"txHash": "0x123...789",
"blockNumber": 42,
"compositeHash": "0xabc...def",
"signer": "0xWalletAddress",
"previousAnchorId": "0x00000000000000000000000000000000",
"timestamp": 1711094400,
"chain": [
{
"anchorId": "0x0123456789abcdef0123456789abcdef",
"txHash": "0x123...789",
"blockNumber": 42,
"compositeHash": "0xabc...def",
"signer": "0xWalletAddress",
"previousAnchorId": "0x00000000000000000000000000000000",
"timestamp": 1711094400
}
],
"encryptedPayload": "<base64url-encoded ciphertext>"
}

The chain array contains all signatures in the document's chain, starting with the requested one and walking back to the first. Each entry represents one DocumentAnchored event from the smart contract. signer is the address that submitted the transaction (the relayer). Returns 404 if the anchor ID was never anchored.

QR URL Format​

HTTPS://VERIFY.GETVEYLENT.COM/V/<anchorId digits>#<key digits>
ComponentRaw sizeEncoded sizeEncoding
Base URL--29 charsUpper case
/V/ separator--3 charsUpper case
Anchor ID16 bytes40 charsTwo big-endian 8-byte halves, each 20 zero-padded digits
# separator--1 charPlain text
Encryption key16 bytes40 charsSame as the anchor ID
Total--113 chars--

Links from earlier builds use https://verify.getveylent.com/v/<base64url(anchorId)>#<base64url(key)>, which verifiers still accept. See QR Code for why digits make a smaller code.

PDF Metadata (Signing History)​

Each signer's layer stores the full signing history as a JSON stream referenced from the document catalog under /SignChainMeta. The key keeps the product's original name so files signed earlier stay readable. Each layer's copy has one more entry than the layer before it.

{
"version": 3,
"signatures": [
{
"signer": "John Doe",
"email": "john@example.com",
"timestamp": "2026-11-03T10:30:00+00:00",
"anchorId": "0x0123456789abcdef0123456789abcdef",
"qrUrl": "HTTPS://VERIFY.GETVEYLENT.COM/V/0008198552921648689500081985529216486895#...",
"placements": [
{ "page": 1, "x": 72.0, "y": 96.0, "width": 120.0, "height": 40.0 }
],
"textFields": [],
"signerType": "individual",
"company": null,
"position": null,
"geo": [6.9271, 79.8612]
}
]
}

previousAnchorId is present on every entry after the first and holds the previous signer's anchor ID.

An entry is written into the same layer it describes, before that layer is hashed, so it can't hold the layer's own document hash. That hash lives in the sealed record, found through anchorId and opened with the key in qrUrl. The entry itself is plain and editable, so the desktop app shows names from the sealed record when it opens.

This metadata is used by the desktop app to:

  1. Find each signer's layer (the first save whose history has that signer's entry last)
  2. Get each signer's anchor ID and the key to their sealed record
  3. Check that the entries link in order before another signer extends the file

Files with a lower version, or entries without anchorId, are legacy. Their older fields (docHash, prevDocHash, inputFileHash, eofByteOffset, compositeHash, txHash, salt) are still parsed, but the files are reported as legacy and can't be signed further.