Skip to content
AXOWLDevelopers
Quickstart

Integrity SDK

Terminal window
dotnet add package Axowl.Sdk.Integrity.Client
builder.Services.AddAxowlIntegrityClient(opts =>
{
opts.ApiKey = builder.Configuration["Axowl:ApiKey"]!; // org API key (ah_live_...)
opts.OrganizationId = myOrganizationId; // default owner of the records you seal
opts.Transport = TransportMode.GrpcWithFallback;
});
OptionDefaultWhat it is
ApiKey— (required)Org API key, sent as Authorization: Bearer ….
OrganizationIdemptyThe organization that owns the records you seal. Leave empty only if every seal call passes its own.
TransportTransportMode.GrpcGrpc, GrpcWithFallback (gRPC first, REST when gRPC cannot get through) or RestOnly.
ServerAddresshttps://grpc.axowl.comgRPC endpoint.
RestServerAddresshttps://api.axowl.comREST endpoint (POST /v1/integrity/seal, POST /v1/integrity/verify).
CallTimeout10 secondsDeadline for each call.

Two layers. IEntitySealer is the one you normally use: it seals a record on create, re-seals it after an edit, and owns the chain bookkeeping described below. IAxowlIntegrityClient is the transport underneath — SealAsync and VerifyAsync. The Axowl.Sdk.Integrity.Hashing package holds the hasher (Sha256Hasher) on its own, for code that only needs to recompute a record hash.

Reach for the transport directly only when you are managing sequence numbers yourself — an append-only event log, for instance, where the chain is per-log rather than per-record.

Every Seal call anchors into a server-side chain addressed by (organization, entityType, entityId, sequenceNumber), and that key is unique: two seals may never claim the same position. The server also verifies continuity — your request’s previousHash must equal the stored hash of position sequenceNumber - 1, or the seal is refused with FAILED_PRECONDITION.

Two rules follow:

  1. An entity and its event log are two chains — give them two keys. The entity itself seals as (entityType, entityId, 1..n). If you also seal an append-only event log about that entity (created / transferred / redeemed …), anchor those under a namespaced type — the convention is "{entityType}:events" with the same entityId and the event’s own sequence. Reusing the entity’s plain type collides with its genesis anchor on the very first event: the first seal is rejected as a duplicate and every later one fails the continuity check.

  2. Retries are safe; overwrites are not. Sending the same (key, clientComputedHash) twice — e.g. a retry after a network timeout — returns the existing anchor (idempotent). Sending the same key with a different hash is refused with ALREADY_EXISTS: a chain position, once anchored, cannot be rewritten. Editing a sealed record is therefore never an overwrite — it claims the next position. See below.

A sealed record is not frozen. When a field inside the sealed payload changes, seal it again at the next chain position: Axowl ends up holding one anchor per version, so the edit is recorded, not hidden. Changing a price on a sealed listing leaves a mark instead of erasing one — which is the point.

IEntitySealer owns those chain rules. Implement ISealedRecord on the entity and call it; do not drive SealAsync by hand.

// on create — chain position 1, previous-hash of 64 zeros
await sealer.SealNewAsync(brand, "Brand", brand.Id, Canonical(brand), ct: ct);
// later — a sealed field changed
brand.Name = "New name";
await sealer.ReSealAsync(brand, "Brand", brand.Id, Canonical(brand), ct: ct);

ReSealAsync is safe to call unconditionally after any edit — it hashes the payload you pass and works out what actually happened:

SituationWhat it does
Sealed payload changedNew chain position, one more anchor
Unchanged, already anchoredNothing — an unchanged save never fabricates a version
Unchanged, anchor failed earlierRetries that position instead of skipping past it
Never sealed (sequence 0)Seals as genesis

So you do not have to diff the payload yourself before deciding whether to re-seal, and a transient anchor failure does not leave a permanent hole in the chain.

Registration comes with the client — AddAxowlIntegrityClient(...) also registers IEntitySealer and a default Sha256Hasher. Set OrganizationId on the options, or pass one per call.

Both methods return a SealOutcome instead of throwing when the anchor call fails, so a failed seal never takes your business action down with it. Sealed tells you whether Axowl anchored the link; on failure the record still carries its RecordHash and chain position (only SealedHash / ChainAnchorId stay null), Error says why, and the next ReSealAsync retries that position. The sealer stamps the record and never touches your database — call it before you save.

Only what you put in the canonical payload is protected, so only a change to those fields needs re-sealing. Display-only columns — media URLs, descriptions, status flags — are normally left out of the payload precisely so they can be edited freely. Decide that split once, when you design the payload:

In the payloadOut of the payload
Economic terms, identity, anything a counterparty relies onMedia URLs, prose descriptions, workflow status
Editing it requires a re-sealEditing it is a plain UPDATE

Keep the payload’s shape stable. Adding or removing a field changes every future hash, and rows sealed under the old shape can no longer be reproduced — they will verify as Mismatch forever. Renaming a property that feeds the payload is the same change.

Position n’s previousHash must equal what the server anchored at n − 1 — the sealedHash it returned, not the recordHash you computed. The continuity check compares against the server’s value, so chain off that one. IEntitySealer already chains off sealedHash, falling back to recordHash only when the previous seal failed and left no anchor to match against.

By default a seal records what happened; pass a SealActor to also record who did it. The actor is a ConnectedId — the badge every subject in your organization holds, your end customers included, not just staff. This is what makes a sealed record usable as billing or attribution evidence: “customer X performed this action” is inside the seal, not in a mutable side column.

// introspect gave you the caller's connected_id and is_employee
var actor = new SealActor(principal.ConnectedId, principal.IsEmployee);
await sealer.SealNewAsync(order, "Order", order.Id, Canonical(order),
actor: actor, ct: ct);

Three things to know:

  1. The claim is hash-covered. When an actor is passed, its two fields join the hashed input (see the V2 shape below), so the “who” cannot be swapped after the fact without breaking the seal. Requests without an actor hash exactly as before — existing chains reproduce unchanged.
  2. The server checks the badge. The actor_connected_id must belong to the request’s organization — a badge from anywhere else is rejected with PERMISSION_DENIED. is_employee must match the badge — pass the value a fresh introspect response gave you; a value that does not match is rejected with INVALID_ARGUMENT.
  3. Persist the actor on your row. Reproducing the record hash later needs the same inputs — store the actor fields alongside the payload, or verification of that version becomes impossible.

This is notarization-level: the seal fixes what the API caller claimed about the actor at seal time, checked against the badge as described above. It is not a signature made by the actor.

Verify returns the recorded actor’s kind next to its badge: actor_type (REST actorType, .NET VerifyResponse.ActorType) is human, ai, service or group. You never send it — Axowl reads it from the badge when the seal is made and stores it with that position, so a caller cannot seal an AI agent’s action as a person’s. It is a snapshot: changing the badge later does not rewrite past positions.

var r = await client.VerifyAsync(request, ct);
if (r.IsGenuine && r.ActorType == SealActorType.Ai)
Console.WriteLine($"Unmodified — sealed for AI agent {r.ActorConnectedId}");

It is empty (Unknown in .NET) when no actor was passed, and for positions sealed before 8 November 2026. It is not part of the hashed record.

A status column in your own database cannot prove a record: whoever can change the row can change the column. Verification means recomputing the hash from the record as it stands now and asking Axowl whether that is what it anchored.

var hash = hasher.ComputeHash("Brand", brand.Id, Canonical(brand),
brand.PreviousHash, brand.SequenceNumber); // IHashCalculator
var r = await client.VerifyAsync(
new VerifyRequest("Brand", brand.Id, organizationId, ClientComputedHash: hash), ct);
if (!r.IsGenuine) { /* r.Verdict says why */ }
VerdictMeaning
MatchThe anchored hash equals the hash you sent. IsGenuine is true only here.
MismatchAn anchor exists and does not match — the record changed after sealing.
NotAnchoredNothing was ever anchored at this position. Unsealed, not valid.
NotCheckedAn anchor exists but you sent no hash, so nothing was compared.

SequenceNumber on the request defaults to 0 = the latest anchor, which is what an edited (re-sealed) record wants; pin a number to audit one specific version. The response also carries AnchoredHash, SealedAt (Axowl’s clock at sealing), SequenceNumber, LatestSequenceNumber (so you can tell a matching but stale version), ActorConnectedId and ActorType. If the record was sealed with an actor, pass the same SealActor to ComputeHash. Verify is read-only — it never creates or moves a chain position.

Anything you sealed, anyone can re-check. The content hash is SHA-256 (lowercase hex) over a JSON record of five inputs, serialized without whitespace in this property order (computed by Sha256Hasher):

{"EntityType":"{entityType}:events","EntityId":"<guid, N format>",
"CanonicalPayload":"<the exact stored payload string>",
"PreviousHash":"<prior event's hash, or 64 zeros>","SequenceNumber":n}

Versions sealed with an actor use the V2 shape — the same five fields followed by the two actor fields:

{"EntityType":"…","EntityId":"…","CanonicalPayload":"…","PreviousHash":"…",
"SequenceNumber":n,"ActorConnectedId":"<guid, N format>","ActorIsEmployee":false}

Three independent checks make a verified chain, and they fail for different reasons — report them separately:

  1. Content — recomputing the five-input hash reproduces the stored recordHash.
  2. Linkage — event n’s previousHash equals the hash anchored for event n − 1 (genesis links to 64 zeros), and no two events claim the same sequence.
  3. Anchor — Axowl anchored the event when it happened: the row carries the SealedHash and ChainAnchorId the seal returned, and VerifyAsync answers Match for it.

Store the canonical string verbatim. The hash covers the payload’s raw bytes, so the column you verify from must return exactly what you hashed. A Postgres jsonb column does not: it reorders keys and normalizes spacing on INSERT, so every content re-check fails on data that was never touched. Use text.