callydus/sign
Permissionless verification

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.

This documentation is for developers and architects integrating document verification into their own systems — ERPs, legal portals, HR systems, etc. Scope is strictly read-only. The code uses only @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.

Solana blockchain
DocumentRecord · SigningEvent · immutable · permissionless
Backend off-chain
title · emails · filename — internal, not externally accessible
invariant: on-chain always wins over off-chain in any conflict

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.

The off-chain API is not an integration surface. Integrators work exclusively with the Solana RPC. What is in the internal database (title, emails) is product data, not blockchain data.

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:

  1. Compute the SHA-256 of the file locally
  2. Derive the PDA address from the hash
  3. Query the blockchain — if an account exists with that hash and Completed, the document has been signed
Trade-off: if the original file is lost, the on-chain proof still exists, but it is not possible to link new files to the proof without the original. The on-chain hash is the sole link between the file and the proof.

Document lifecycle

Every DocumentRecord transitions through the following states:

Pending
initial state
first Approved event
PartialySigned
all signatories approved
Completed
terminal
any signatory declined
Declined
terminal
Pending also transitions directly to Declined if the sole signatory declines on the first event. Completed and Declined are terminal — no instruction can be called after either.

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

Sequential
Signatories must sign in the exact order they were listed at creation. The program rejects with WrongSignatoryOrder any out-of-order attempt. Ensures, for example, that an approver signs before the auditor.
Parallel
All signatories can sign in any order. The document moves to Completed when the last one signs. Any signatory can decline at any time.

In both modes, any signatory can decline at any time, ending the flow with Declined.

Integration guide

This integration is strictly read-only. 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:

1Compute the SHA-256 of the file locally
2Derive the PDA address of the DocumentRecord
3Fetch the account via RPC and deserialize the Anchor discriminator
4Read the status, signatories, and timestamps
5(Optional) Fetch the SigningEvents for each signatory

Full TypeScript example

verify-document.ts
// 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 };
  }));
}
This code uses only @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

FD8rXcGgzMsgUNqpexKfGz4CuFPY2urvsML69rf37ifndevnet

PDA seeds

DocumentRecord
seeds: ["document", document_hash]
SigningEvent
seeds: ["signing-event", document_record_pda, signatory_pubkey]

DocumentRecord — on-chain fields

fieldtypedescription
document_hash[u8; 32]SHA-256 of the original file
initiatorPubkeyWallet that created the document
signatoriesVec<Pubkey>List of signatory wallets (max. 10)
modeenumSequential or Parallel
statusenumPending | PartialySigned | Completed | Declined
approved_countu8Number of approvals so far
created_ati64 (unix ts)Creation timestamp of the DocumentRecord

SigningEvent — on-chain fields

fieldtypedescription
document_recordPubkeyPDA of the associated DocumentRecord
signerPubkeyWallet that performed the action
signature_typeenumApproved or Declined
signed_ati64 (unix ts)Timestamp of the signing or decline action
Both accounts are immutable once created. The status field of DocumentRecord is updated with each SigningEvent.
callydus/sign — docsThe proof is public. The interface is optional.