Register a Record
Step-by-step guide to registering a content hash and creating a record in the Integra protocol.
This guide walks you through the full record registration flow: generating a content hash, registering it in the existence layer, creating a record with ownership and metadata, and configuring the record with resolvers and executors.
How It Works
Record registration is a two-layer process:
- Existence layer (
IntegraExistenceV1) -- immutable, write-once proof that content existed at a specific time. No admin, no fees, no access control. Anyone can register. First caller wins. - Record layer (
IntegraRecordV1) -- adds ownership, executor delegation, tokenizer association, and resolver bindings on top of the existence proof.
When you call register() or registerSimple() on IntegraRecordV1, it automatically registers the content hash in the existence layer if it has not been registered yet. You do not need to call both contracts separately.
Step 1: Generate the Content Hash
The content hash is a bytes32 fingerprint of your content. The protocol does not prescribe a specific algorithm -- keccak256 and sha256 are both common.
In Solidity
// Hash raw document bytes
bytes32 contentHash = keccak256(abi.encodePacked(documentBytes));
// Or hash a combination of content location + metadata
bytes32 contentHash = keccak256(abi.encodePacked(ipfsCid, metadata));In TypeScript (ethers.js v6)
import { keccak256, toUtf8Bytes } from "ethers";
// Hash a string
const contentHash = keccak256(toUtf8Bytes("Agreement between Alice and Bob..."));
// Hash raw bytes (e.g., file contents)
const fileBuffer = fs.readFileSync("contract.pdf");
const contentHash = keccak256(fileBuffer);In TypeScript (viem)
import { keccak256, toBytes } from "viem";
const contentHash = keccak256(toBytes("Agreement between Alice and Bob..."));The content itself never goes on-chain. Store it wherever makes sense for your application -- IPFS, a database, cloud storage -- and keep the hash as the on-chain reference.
Step 2: Register in the Existence Layer
IntegraExistenceV1 is the simplest contract in the protocol. It has one write function:
function register(bytes32 contentHash) external returns (uint64 timestamp);This is permissionless -- any address can call it. Once registered, the hash can never be re-registered or modified. The first caller becomes the permanent registrant.
Direct Registration (Optional)
You can register a content hash directly if you want existence proof without creating a full record:
// ethers.js v6
const tx = await existence.register(contentHash);
const receipt = await tx.wait();
// Parse the Registered event
const event = receipt.logs.find(
(log) => existence.interface.parseLog(log)?.name === "Registered"
);
const { timestamp, registeredBy } = existence.interface.parseLog(event).args;
console.log(`Registered at ${new Date(Number(timestamp) * 1000)}`);Batch Registration
For bulk operations, registerBatch() registers multiple hashes in one transaction. Duplicates are silently skipped -- no need to pre-filter:
const hashes = documents.map((doc) => keccak256(doc));
const tx = await existence.registerBatch(hashes);
const receipt = await tx.wait();
console.log(`Registered ${receipt.logs.length} new hashes`);Checking Existence
const isRegistered = await existence.exists(contentHash);
if (isRegistered) {
const [registeredAt, registeredBy] = await existence.getRegistration(contentHash);
console.log(`Registered at ${new Date(Number(registeredAt) * 1000)} by ${registeredBy}`);
}Step 3: Create a Record
IntegraRecordV1 is the primary contract you interact with for document management. It provides three registration methods:
| Method | Use Case |
|---|---|
registerSimple() | Most common -- no tokenizer, no resolver, no ZK references |
register() | Full-featured -- supports tokenizers, resolvers, identity extensions, ZK parent references |
registerBatch() | Bulk onboarding -- up to 50 records in one transaction |
Using registerSimple
The simplest path for creating a record:
function registerSimple(
bytes32 integraHash, // Unique record identifier (you generate this)
bytes32 contentHash, // Content fingerprint
address authorizedExecutor, // Delegate address (address(0) for owner-only)
bytes32 processHash, // Workflow correlation ID (must be non-zero)
address paymentToken, // Fee token (address(0) for ETH)
address owner // Record owner (address(0) defaults to msg.sender)
) external payable returns (bytes32);In Solidity
import {IntegraRecordV1} from "./registry/IntegraRecordV1.sol";
// Generate unique identifiers
bytes32 contentHash = keccak256(abi.encodePacked(documentBytes));
bytes32 integraHash = keccak256(abi.encodePacked(contentHash, msg.sender, block.timestamp));
bytes32 processHash = keccak256(abi.encodePacked("workflow-", integraHash));
// Register the record (auto-registers content hash in existence layer)
record.registerSimple{value: registrationFee}(
integraHash,
contentHash,
address(0), // no executor
processHash,
address(0), // pay in ETH
address(0) // owner = msg.sender
);In TypeScript (ethers.js v6)
import { ethers, keccak256, toUtf8Bytes, randomBytes, AbiCoder } from "ethers";
const contentHash = keccak256(toUtf8Bytes("Agreement content..."));
// Generate a unique integraHash
const integraHash = keccak256(
AbiCoder.defaultAbiCoder().encode(
["bytes32", "address", "uint256"],
[contentHash, signer.address, Date.now()]
)
);
// Generate a process correlation ID
const processHash = keccak256(randomBytes(32));
const tx = await record.registerSimple(
integraHash,
contentHash,
ethers.ZeroAddress, // no executor
processHash,
ethers.ZeroAddress, // pay in ETH
ethers.ZeroAddress, // owner = msg.sender
{ value: ethers.parseEther("0.001") }
);
const receipt = await tx.wait();
console.log("Record created:", integraHash);Using register (Full-Featured)
For advanced use cases that need tokenizers, resolvers, or ZK parent references:
struct RegistrationParams {
bytes32 integraHash; // Unique record identifier
bytes32 contentHash; // Content hash
bytes32 identityExtension; // Optional: ZK commitments, DIDs, cross-chain refs
bytes32 referenceHash; // Optional: parent document reference (requires ZK proof)
address tokenizer; // Optional: tokenizer contract address
bytes32 primaryResolverId; // Optional: primary resolver component ID
address authorizedExecutor; // Optional: executor delegate
bytes32 processHash; // Required: workflow correlation ID
address paymentToken; // Fee token (address(0) for ETH)
address owner; // Owner (address(0) defaults to msg.sender)
}
function register(
RegistrationParams calldata params,
ProofData calldata proof // ZK proof (zeroed if no referenceHash)
) external payable returns (bytes32);const params = {
integraHash,
contentHash,
identityExtension: ethers.ZeroHash, // no extension
referenceHash: ethers.ZeroHash, // no parent reference
tokenizer: OWNERSHIP_TOKENIZER_ADDRESS, // associate a tokenizer at creation
primaryResolverId: COMPLIANCE_RESOLVER_ID, // attach a resolver
authorizedExecutor: EXECUTOR_ADDRESS, // delegate operations
processHash,
paymentToken: ethers.ZeroAddress, // pay in ETH
owner: ethers.ZeroAddress, // defaults to msg.sender
};
// ZK proof (all zeros when no referenceHash)
const proof = {
a: [0n, 0n],
b: [[0n, 0n], [0n, 0n]],
c: [0n, 0n],
};
const tx = await record.register(params, proof, {
value: ethers.parseEther("0.001"),
});Using registerBatch (Bulk)
For onboarding many records at once (up to 50 per transaction):
const integraHashes = documents.map((_, i) =>
keccak256(AbiCoder.defaultAbiCoder().encode(
["bytes32", "uint256"], [contentHashes[i], i]
))
);
const processHashes = documents.map(() => keccak256(randomBytes(32)));
const tx = await record.registerBatch(
integraHashes,
contentHashes,
integraHashes.map(() => ethers.ZeroHash), // no identity extensions
integraHashes.map(() => ethers.ZeroAddress), // no tokenizers
integraHashes.map(() => ethers.ZeroHash), // no resolvers
ethers.ZeroAddress, // no shared executor
processHashes,
integraHashes.map(() => ethers.ZeroAddress), // owner = msg.sender
false, // skip resolver hooks for gas efficiency
ethers.ZeroAddress, // pay in ETH
{ value: ethers.parseEther("0.05") } // fee * 50 records
);Step 4: Configure the Record
After creating a record, the owner can configure it further.
Associate a Tokenizer
Tokenizer association is a one-time operation -- once set, it cannot be changed. The tokenizer must be registered in IntegraRegistryV1.
const tx = await record.associateTokenizer(
integraHash,
OWNERSHIP_TOKENIZER_ADDRESS,
processHash
);Add Resolvers
Each record supports up to 10 resolvers for compliance, automation, dispute resolution, and metadata:
// Add a compliance resolver
await record.addResolver(integraHash, COMPLIANCE_RESOLVER_ID);
// Add a contact resolver
await record.addResolver(integraHash, CONTACT_RESOLVER_ID);Authorize an Executor
An executor is a per-record delegate that can act on behalf of the owner in downstream contracts (tokenizers, resolvers). Only one executor per record at a time:
// Authorize an executor
await record.authorizeExecutor(integraHash, executorAddress);
// Later, replace with a different executor (atomic swap)
await record.replaceExecutor(integraHash, newExecutorAddress);
// Or revoke entirely
await record.revokeExecutor(integraHash);Query the Record
// Check if a record exists
const exists = await record.recordExists(integraHash);
// Get the record owner
const owner = await record.getOwner(integraHash);
// Get the full record details
const details = await record.getRecord(integraHash);
console.log("Owner:", details.owner);
console.log("Content hash:", details.contentHash);
console.log("Tokenizer:", details.tokenizer);
console.log("Registered at:", new Date(Number(details.registeredAt) * 1000));Understanding integraHash vs contentHash
These two identifiers serve different purposes:
| integraHash | contentHash | |
|---|---|---|
| Purpose | Record primary key | Content fingerprint |
| Uniqueness | Must be globally unique | Can be shared across records |
| Who generates it | You (the developer) | Derived from your content |
| Where it lives | IntegraRecordV1 | IntegraExistenceV1 + IntegraRecordV1 |
| Mutability | Fixed at creation | Fixed at creation |
The contentHash is registered in the immutable existence layer. The integraHash is the key you use for all subsequent operations -- transferring ownership, associating tokenizers, adding resolvers, and delegating to executors.