Skip to main content

Signing Reference

Every business event appended to Genesis X-1 carries an Ed25519 actor signature. This signature is what makes the event independently verifiable — any party with the actor's public key can confirm who authorized the event, what was authorized, and that nothing was changed after signing.

This document provides the exact signing formula, canonical JSON definition, and working implementations in Node.js, Python, and Go.

Signing Formula

message = eventType + 0x00 + ledgerID + 0x00 + canonicalJSON(payload)
digest = SHA-256(message)
sig = Ed25519.Sign(privateKey, digest)
header = X-Actor-Sig: base64(sig)

Canonical JSON — keys sorted A→Z at every nesting level, no spaces between tokens.

The 0x00 byte is a null separator between fields. It prevents ambiguity when field values contain adjacent characters.

Node.js

import { createHash, createPrivateKey, sign } from 'crypto';
import { readFileSync } from 'fs';

const privateKeyPem = readFileSync(process.env.IAEX_PRIVATE_KEY_PATH, 'utf8');

// Sort all object keys recursively — required to match server CanonicalJSON.
// JSON.stringify(obj, sortedKeys) only sorts top-level; this handles nested objects.
function canonicalJSON(value) {
if (Array.isArray(value)) {
return '[' + value.map(canonicalJSON).join(',') + ']';
}
if (value !== null && typeof value === 'object') {
const keys = Object.keys(value).sort();
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(value[k])).join(',') + '}';
}
return JSON.stringify(value);
}

function buildSignature(eventType, ledgerID, payload) {
const canon = canonicalJSON(payload);
const message = Buffer.concat([
Buffer.from(eventType, 'utf8'),
Buffer.from([0x00]),
Buffer.from(ledgerID, 'utf8'),
Buffer.from([0x00]),
Buffer.from(canon, 'utf8'),
]);
const digest = createHash('sha256').update(message).digest();
const privKey = createPrivateKey(privateKeyPem);
return sign(null, digest, privKey).toString('base64');
}

Complete Client Class

export class IAEXClient {
constructor({ apiKey, baseUrl = 'https://api.iaexnetwork.com/v1', privateKeyPem }) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.privateKeyPem = privateKeyPem;
}

sign(eventType, ledgerID, payload) {
return buildSignature(eventType, ledgerID, payload);
}

async request(method, path, body, sig) {
const headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
};
if (sig) headers['X-Actor-Sig'] = sig;
const res = await fetch(this.baseUrl + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw await res.json();
return res.json();
}

openLedger(body) {
// No signature — ledger ID is server-generated.
return this.request('POST', '/ledgers', body, null);
}

createMaster(ledgerID, businessRef, scope) {
const sigPayload = { business_ref: businessRef, ledger_id: ledgerID, scope };
const sig = buildSignature('TRACELEDGER_MASTER_CREATED', ledgerID, sigPayload);
return this.request('POST', '/traceledger/master', {
ledger_id: ledgerID, business_ref: businessRef, scope,
}, sig);
}

appendEvent(masterUUID, ledgerID, eventType, payload) {
const sig = buildSignature(eventType, ledgerID, payload);
return this.request('POST', `/traceledger/master/${masterUUID}/events`, {
event_type: eventType, payload,
}, sig);
}
}

Python

import hashlib, base64, json, os
import httpx
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def load_private_key() -> Ed25519PrivateKey:
with open(os.environ['IAEX_PRIVATE_KEY_PATH'], 'rb') as f:
return load_pem_private_key(f.read(), password=None)

def build_signature(private_key: Ed25519PrivateKey, event_type: str, ledger_id: str, payload: dict) -> str:
canon = json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()
message = event_type.encode() + b'\x00' + ledger_id.encode() + b'\x00' + canon
digest = hashlib.sha256(message).digest()
return base64.b64encode(private_key.sign(digest)).decode()


class IAEXClient:
def __init__(self, api_key: str, base_url: str = 'https://api.iaexnetwork.com/v1'):
self.api_key = api_key
self.base_url = base_url
self.private_key = load_private_key()
self.http = httpx.Client(base_url=base_url)

def _headers(self, sig: str = None) -> dict:
h = {'Authorization': f'Bearer {self.api_key}'}
if sig:
h['X-Actor-Sig'] = sig
return h

def open_ledger(self, buyer_actor_id, buyer_org_id, supplier_actor_id, supplier_org_id):
r = self.http.post('/ledgers', headers=self._headers(), json={
'buyer_actor_id': buyer_actor_id,
'buyer_organization_id': buyer_org_id,
'supplier_actor_id': supplier_actor_id,
'supplier_organization_id': supplier_org_id,
})
r.raise_for_status()
return r.json()

def create_master(self, ledger_id: str, business_ref: str, scope: str):
sig_payload = {'business_ref': business_ref, 'ledger_id': ledger_id, 'scope': scope}
sig = build_signature(self.private_key, 'TRACELEDGER_MASTER_CREATED', ledger_id, sig_payload)
r = self.http.post('/traceledger/master',
headers=self._headers(sig),
json={'ledger_id': ledger_id, 'business_ref': business_ref, 'scope': scope})
r.raise_for_status()
return r.json()

def append_event(self, master_uuid: str, ledger_id: str, event_type: str, payload: dict):
sig = build_signature(self.private_key, event_type, ledger_id, payload)
r = self.http.post(f'/traceledger/master/{master_uuid}/events',
headers=self._headers(sig),
json={'event_type': event_type, 'payload': payload})
r.raise_for_status()
return r.json()


# Usage
client = IAEXClient(api_key=os.environ['IAEX_API_KEY'])

ledger = client.open_ledger('actor-a', 'org-a', 'actor-b', 'org-b')
master = client.create_master(ledger['id'], 'PO-2026-001', 'ORDER')
event = client.append_event(master['uuid'], ledger['id'], 'SHIPMENT_DISPATCHED', {
'carrier': 'BlueDart',
'tracking_number': 'BD123456789',
'quantity_kg': 500,
})

Go

package iaex

import (
"bytes"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
)

func BuildSignature(privKey ed25519.PrivateKey, eventType, ledgerID string, payload map[string]any) (string, error) {
canon, err := json.Marshal(payload) // Go sorts map keys A→Z
if err != nil {
return "", fmt.Errorf("marshal: %w", err)
}
var msg []byte
msg = append(msg, eventType...)
msg = append(msg, 0x00)
msg = append(msg, ledgerID...)
msg = append(msg, 0x00)
msg = append(msg, canon...)
digest := sha256.Sum256(msg)
return base64.StdEncoding.EncodeToString(ed25519.Sign(privKey, digest[:])), nil
}

type Client struct {
BaseURL string
APIKey string
PrivKey ed25519.PrivateKey
HTTP *http.Client
}

func (c *Client) do(method, path string, body any, sig string) ([]byte, error) {
b, _ := json.Marshal(body)
req, _ := http.NewRequest(method, c.BaseURL+path, bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+c.APIKey)
req.Header.Set("Content-Type", "application/json")
if sig != "" {
req.Header.Set("X-Actor-Sig", sig)
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}

func (c *Client) AppendEvent(masterUUID, ledgerID, eventType string, payload map[string]any) ([]byte, error) {
sig, err := BuildSignature(c.PrivKey, eventType, ledgerID, payload)
if err != nil {
return nil, err
}
return c.do("POST", "/traceledger/master/"+masterUUID+"/events", map[string]any{
"event_type": eventType,
"payload": payload,
}, sig)
}

Partial Signing Reference

Some operations involve server-generated fields (IDs, timestamps) that cannot be known before the call. For these, sign only the fields the caller provides. Keys sorted A→Z in all cases.

Formula applies in all cases: SHA-256(eventType + 0x00 + ledgerID + 0x00 + canonicalJSON(sigPayload))

OperationEvent type (exact)ledgerIDSign over
Create masterTRACELEDGER_MASTER_CREATEDLedger the master belongs to{business_ref, ledger_id, scope}
Grant org delegationSUPPLIER_DELEGATION_GRANTEDOrg's FRL ledger ID{delegate_actor_id, organization_id, permissions, role}
Revoke org delegationSUPPLIER_DELEGATION_REVOKEDOrg's FRL ledger ID{delegate_actor_id, organization_id}
Grant ledger delegationLEDGER_DELEGATION_GRANTEDThe ledger being delegated{delegate_actor_id, ledger_id, permissions, role}
Revoke ledger delegationLEDGER_DELEGATION_REVOKEDThe ledger being delegated{delegate_actor_id, ledger_id}
Grant master accessMASTER_DELEGATION_GRANTEDParent ledger of the master{delegate_actor_id, permissions, role, traceledger_master_id}
Revoke master accessMASTER_DELEGATION_REVOKEDParent ledger of the master{delegate_actor_id, traceledger_master_id}
All other eventsYour UPPER_SNAKE_CASE stringThe event's ledgerFull payload

For master delegation: use the ledger_id of the ledger the master belongs to, not the master UUID. Retrieve it from GET /traceledger/master/{uuid}ledger_id.

Operations That Do Not Require X-Actor-Sig

OperationReason
POST /onboarding/organizationAuthority seals all structural events
POST /entities/onboardActor being created — no prior key
POST /entities/root-ledgerNo keys enrolled at onboard time in Genesis X-1
POST /engagementsAuthority seals GENESIS
POST /ledgersLedger ID is server-generated

Debugging

# 1. Print canonical JSON — verify key sort order
import json
print(json.dumps(payload, sort_keys=True, separators=(',', ':')))

# 2. Print SHA-256 digest (hex) — compare with server logs
import hashlib
canon = json.dumps(payload, sort_keys=True, separators=(',', ':')).encode()
msg = event_type.encode() + b'\x00' + ledger_id.encode() + b'\x00' + canon
print(hashlib.sha256(msg).hexdigest())

Common errors:

ErrorCause
"no signing key enrolled"Enroll key via POST /actors/{id}/keys first
"X-Actor-Sig header required"Key enrolled but header missing
"actor_sig verification failed"Wrong key sort order, missing SHA-256 step, or wrong ledgerID