You don't need Callydus Sign to verify a document.
The signing proof is public, immutable, and independent of this platform. Any system can verify the authenticity of a document with a direct RPC call to Solana — no account needed, no API key, no fee, no permission required from anyone.
You need the original file, an RPC call, and the IDL. That's it. This page explains what lives where, how to verify, and what to expect in non-trivial situations.
@solana/web3.js with no Callydus Sign SDK dependency.How it works
The system has two layers: one on-chain, immutable and permissionless; one off-chain, internal and irrelevant to integrators.
When a document is submitted to Callydus Sign, only the SHA-256 hash is sent to the blockchain. The original file never leaves the user's device. On the blockchain, two account types are created: a DocumentRecord and one SigningEvent per signing action.
The off-chain backend stores only internal UX metadata — title, emails, filename. These exist for platform notifications and dashboard. Not externally accessible. The off-chain API is restricted to platform origins; external requests receive 403. The source of truth for who signed what and when is exclusively on-chain.
Privacy invariant
Callydus Sign has no access to any document content — not even encrypted. This is a permanent design decision, not a planned feature.
To verify a document, the verifier must have the original file. The process is:
- Compute the SHA-256 of the file locally
- Derive the PDA address from the hash
- Query the blockchain — if an account exists with that hash and Completed, the document has been signed
Document lifecycle
Every DocumentRecord transitions through the following states:
The approved_count field in DocumentRecord tracks how many signatories have approved. In Sequential mode, it also indicates who is next in queue.
Signing modes
WrongSignatoryOrder any out-of-order attempt. Ensures, for example, that an approver signs before the auditor.In both modes, any signatory can decline at any time, ending the flow with Declined.
Integration guide
create_document, sign_document and decline_document are Solana transactions that require the user to sign with their own wallet in real time — there is no REST endpoint that does this on anyone's behalf. The only supported operation here is verification: querying the state of an existing document on-chain.Verification is done directly via Solana RPC, without going through Callydus Sign. You need:
@solana/web3.js— standard Solana ecosystem library- The original document file
- Access to a Solana RPC endpoint (devnet or mainnet)
The full process has 5 steps:
Full TypeScript example
// Permissionless verification — @solana/web3.js, sem SDK Callydus
import { Connection, PublicKey } from "@solana/web3.js";
import * as crypto from "crypto";
import { readFileSync } from "fs";
const PROGRAM_ID = new PublicKey(
"FD8rXcGgzMsgUNqpexKfGz4CuFPY2urvsML69rf37ifn"
);
const RPC = new Connection("https://api.devnet.solana.com");
// Step 1 — compute SHA-256 of the file locally
function hashFile(path: string): Buffer {
const bytes = readFileSync(path);
return crypto.createHash("sha256").update(bytes).digest();
}
// Step 2 — derive the DocumentRecord PDA address
// PDA seeds: ["document", document_hash] — determinístico a partir do hash
async function deriveDocumentPDA(hash: Buffer): Promise<PublicKey> {
const [pda] = await PublicKey.findProgramAddress(
[Buffer.from("document"), hash],
PROGRAM_ID
);
return pda;
}
// Step 3 — fetch and deserialize the account
// Borsh layout after Anchor discriminator (8 bytes):
// [u8;32] document_hash | Pubkey initiator | Vec<Pubkey> signatories
// u8 mode | i64 created_at | u8 status | u8 approved_count | u8 bump
type Status = "Pending" | "PartialySigned" | "Completed" | "Declined";
interface DocumentRecord {
documentHash: string; // hex
initiator: string; // base58
signatories: string[]; // base58[]
mode: "Sequential" | "Parallel";
status: Status;
approvedCount: number; // u8 — readUInt8, não readUInt32LE
createdAt: Date;
}
function parseDocumentRecord(data: Buffer): DocumentRecord {
let offset = 8; // skip Anchor discriminator
const documentHash = data.slice(offset, (offset += 32)).toString("hex");
const initiator = new PublicKey(data.slice(offset, (offset += 32))).toBase58();
const sigLen = data.readUInt32LE(offset);
offset += 4;
const signatories: string[] = [];
for (let i = 0; i < sigLen; i++) {
signatories.push(new PublicKey(data.slice(offset, (offset += 32))).toBase58());
}
const MODES = ["Sequential", "Parallel"] as const;
const STATUSES = ["Pending", "PartialySigned", "Completed", "Declined"] as const;
const mode = MODES[data.readUInt8(offset++)];
const createdAt = new Date(Number(data.readBigInt64LE(offset)) * 1000);
offset += 8;
const status = STATUSES[data.readUInt8(offset++)];
const approvedCount = data.readUInt8(offset++);
return { documentHash, initiator, signatories, mode, status, approvedCount, createdAt };
}
// Step 4 — verify the document
async function verifyDocument(filePath: string) {
const hash = hashFile(filePath);
const pda = await deriveDocumentPDA(hash);
const accountInfo = await RPC.getAccountInfo(pda);
if (!accountInfo) {
return { verified: false, reason: "account not found" };
}
const record = parseDocumentRecord(Buffer.from(accountInfo.data));
if (record.status !== "Completed") {
return { verified: false, reason: "status: " + record.status };
}
// Step 5 (optional) — fetch individual SigningEvents
const events = await fetchSigningEvents(pda, record.signatories);
return { verified: true, record, events };
}
async function fetchSigningEvents(
documentPda: PublicKey,
signatories: string[]
) {
return Promise.all(signatories.map(async (signatory) => {
// Seeds: ["signing-event", document_record_pda, signatory_pubkey]
const [eventPda] = await PublicKey.findProgramAddress([
Buffer.from("signing-event"),
documentPda.toBuffer(),
new PublicKey(signatory).toBuffer(),
], PROGRAM_ID);
const info = await RPC.getAccountInfo(eventPda);
return { signatory, data: info?.data };
}));
}@solana/web3.js and native Node.js modules. No Callydus Sign SDK dependency required.Edge cases
What to expect in non-trivial situations. Read this before going to production.
Reference
IDL
The Anchor program IDL (Interface Description Language) is publicly available at [domain]/idl.json. It describes all instructions, account structs, and errors of the smart contract — with it, you can use standard Solana libraries to deserialize accounts without writing a manual parser.
Program ID
FD8rXcGgzMsgUNqpexKfGz4CuFPY2urvsML69rf37ifndevnetPDA seeds
seeds: ["document", document_hash]seeds: ["signing-event", document_record_pda, signatory_pubkey]DocumentRecord — on-chain fields
| field | type | description |
|---|---|---|
| document_hash | [u8; 32] | SHA-256 of the original file |
| initiator | Pubkey | Wallet that created the document |
| signatories | Vec<Pubkey> | List of signatory wallets (max. 10) |
| mode | enum | Sequential or Parallel |
| status | enum | Pending | PartialySigned | Completed | Declined |
| approved_count | u8 | Number of approvals so far |
| created_at | i64 (unix ts) | Creation timestamp of the DocumentRecord |
SigningEvent — on-chain fields
| field | type | description |
|---|---|---|
| document_record | Pubkey | PDA of the associated DocumentRecord |
| signer | Pubkey | Wallet that performed the action |
| signature_type | enum | Approved or Declined |
| signed_at | i64 (unix ts) | Timestamp of the signing or decline action |
status field of DocumentRecord is updated with each SigningEvent.