Skip to main content

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:

  1. 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.
  2. 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:

MethodUse 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);

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:

integraHashcontentHash
PurposeRecord primary keyContent fingerprint
UniquenessMust be globally uniqueCan be shared across records
Who generates itYou (the developer)Derived from your content
Where it livesIntegraRecordV1IntegraExistenceV1 + IntegraRecordV1
MutabilityFixed at creationFixed 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.

Next Steps