On this page

Documentation

Integrations.

Give every organization your platform serves an audit trail anyone can check: what happened, when, and what state it was in each day, recorded on Horizen and proven on zkVerify.

Integrations are for platforms. A hotel system records statement closings and daily operations for each hotel. A compliance platform records approvals and sign-offs, and proves each customer's security posture every day. Your servers send events and daily states over HTTP. Attest runs a wallet for each organization, sends one transaction per record, proves each day in zero knowledge, and gives every record a public verification page.

How it works

Built so every number holds up on chain.

One organization, one wallet

Every organization you register gets its own wallet on Horizen, derived for it alone. Records never share a wallet.

One record, one transaction

Nothing is batched. Each event and each daily state is its own transaction, with its own hash, from its organization's wallet.

A proof every day

Each day's state is committed, chained to the day before, proven in zero knowledge, and verified on zkVerify.

Fingerprints, not data

Only fingerprints and commitments go on chain. Metric values are proven, never stored or revealed.

No crypto to handle

Attest runs the wallets and pays network costs from its treasury. You call an HTTP API.

Checkable by anyone

Every record has a public verification page that checks it live against Horizen and zkVerify.

Two kinds of record. An event says something happened at an organization: a statement was closed, a document signed, evidence approved. A daily state says what an organization looked like on a day: a fingerprint of its full state, plus up to eight private metrics with public checks, proven in zero knowledge. Events give you a timeline; daily states give you continuity, even on days when nothing happens.

Quickstart

Three calls.

DataHubz enables Integrations for your platform; you then create an integration key in Dashboard → Integrations. Keys start with ik_live_, act for your platform (never for a person), and belong on your servers only.

1. register an organization
curl https://attest.datahubz.com/api/v1/organizations \
  -H "Authorization: Bearer $ATTEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalRef": "hotel-42", "name": "Pousada Aysú" }'
2. record an event
curl https://attest.datahubz.com/api/v1/events \
  -H "Authorization: Bearer $ATTEST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: statement-8841-closed" \
  -d '{
    "organization": "hotel-42",
    "type": "statement.closed",
    "subject": "statement-8841",
    "data": { "unit": "101", "period": "2026-09", "netCents": 1845000 }
  }'
3. record the day's state
curl https://attest.datahubz.com/api/v1/snapshots \
  -H "Authorization: Bearer $ATTEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organization": "hotel-42",
    "stream": "hotel.daily",
    "day": "2026-09-27",
    "state": { "...": "the full state you want to fingerprint" },
    "metrics": [
      { "name": "bookings", "value": 14 },
      { "name": "unreconciled_payouts", "value": 0, "check": { "op": "eq", "value": 0 } },
      { "name": "revenue_cents", "value": 1845000 }
    ]
  }'

With the TypeScript client (npm install @datahubz/attest), the same three calls:

typescript
import { Attest } from "@datahubz/attest";

const attest = new Attest({ apiKey: process.env.ATTEST_API_KEY! });

await attest.organizations.upsert({ externalRef: "hotel-42", name: "Pousada Aysú" });

await attest.events.record({
  organization: "hotel-42",
  type: "statement.closed",
  subject: statement.id,
  data: statement,
  idempotencyKey: `statement-${statement.id}-closed`,
});

await attest.snapshots.record({
  organization: "hotel-42",
  stream: "hotel.daily",
  state: dailyState,
  metrics: [
    { name: "bookings", value: kpis.bookings },
    { name: "unreconciled_payouts", value: open.length, check: { op: "eq", value: 0 } },
    { name: "revenue_cents", value: kpis.revenueCents },
  ],
});

Organizations

Every organization you serve, with its own wallet.

Register each organization once, with your own id for it as externalRef. The call is idempotent: repeat it at will (on every sync, for instance) and it updates the name. The organization's wallet is created on the spot, so you can show its address right away. From then on, refer to it by externalRef or by its Attest id (org_…).

response
{
  "id": "org_01K…",
  "object": "organization",
  "externalRef": "hotel-42",
  "name": "Pousada Aysú",
  "wallet": {
    "address": "0x2555…",
    "network": "Horizen",
    "chainId": 26514,
    "explorerUrl": "https://horizen.calderaexplorer.xyz/address/0x2555…"
  },
  "publicUrl": null,
  "excluded": false,
  "createdAt": "2026-09-27T12:00:00.000Z"
}

Wallets are derived for each organization on a hardened path of their own, so no organization's key can be derived from another's. The Attest treasury tops each wallet up with gas when it runs low; the top-ups are ordinary transactions, visible on chain like everything else.

Excluding an organization

To stop recording for an organization (a test organization, or a customer who left), exclude it with PATCH /api/v1/organizations/{ref} and { "excluded": true }, or from the dashboard. It disappears from the transparency pages in your dashboard, and new events and daily states for it are refused with 409 organization_excluded, which a client can treat as a quiet skip. Registering it again doesn't bring it back; { "excluded": false } does. Records already on chain stay on chain.

Events

Things that happened, one transaction each.

Record an event when something with audit value happens: a statement closed or paid, a payout reconciled, a document signed, evidence approved, an auditor's sign-off. The call answers 202 at once; the event is anchored moments later, in its own transaction from the organization's wallet.

FieldRequiredWhat it is
organizationyesexternalRef or org_ id.
typeyesWhat happened: lowercase, dot-separated, e.g. statement.closed, evidence.approved.
dataone ofAny JSON. Attest fingerprints its canonical form (keys sorted) and doesn't keep it.
digestone ofOr send the sha256 yourself (64 hex characters), and the data never leaves your servers.
subjectnoYour id for what it's about, e.g. statement-8841. Up to 200 characters.
occurredAtnoISO 8601. Defaults to now.
metadatanoUp to 20 short labels (strings, numbers, booleans). Returned to you through the API; not shown on verification pages.

Idempotency

Send an Idempotency-Key header (or idempotencyKey in the body) derived from what happened, such as statement-8841-closed. Retrying with the same key returns the original event (200, Idempotent-Replayed: true) instead of recording a second one; the same key with different content is refused with 409. The TypeScript client adds a random key when you don't pass one, which makes its own retries safe; pass yours to make retries across restarts safe too.

What goes on chain

The value anchored for an event is sha256("attest.event.v1|" + id + "|" + digest). It is unique to the event, so every event has its own transaction, and it reveals nothing about the content. Anyone holding the data can recompute the digest and check it against the chain.

event, once anchored
{
  "id": "evt_01K…",
  "object": "event",
  "organization": { "id": "org_01K…", "externalRef": "hotel-42" },
  "type": "statement.closed",
  "subject": "statement-8841",
  "occurredAt": "2026-09-27T12:03:11.000Z",
  "digest": "5e8f…",
  "metadata": null,
  "status": "anchored",
  "anchor": {
    "network": "Horizen",
    "chainId": 26514,
    "value": "a41c…",
    "txHash": "0x7d0e…",
    "blockNumber": "1840021",
    "from": "0x2555…",
    "anchoredAt": "2026-09-27T12:03:15.000Z",
    "explorerUrl": "https://horizen.calderaexplorer.xyz/tx/0x7d0e…"
  },
  "error": null,
  "verifyUrl": "https://attest.datahubz.com/verify/evt_01K…",
  "createdAt": "2026-09-27T12:03:12.000Z"
}

Daily state proofs

What each organization looked like, every day, proven.

Once a day, per organization and stream (a name for the kind of state, such as hotel.daily or monitoring.daily), send the state and up to eight metrics. Attest then does four things:

  1. Commits to the day: Poseidon(organization, stream, day, stateDigest, previous commitment, Poseidon(metrics), salt). Each day includes the previous day's commitment, so the days form a chain no one can rewrite or fill in later.
  2. Proves in zero knowledge (Groth16) that each check's result is true of the hidden metrics. A check that fails is proven to fail; nothing is hidden by leaving it out.
  3. Verifies the proof on zkVerify, where validators check it and record the result.
  4. Anchors the commitment on Horizen, in its own transaction from the organization's wallet. The anchored value is the commitment itself: the same number the proof outputs.
FieldRequiredWhat it is
organizationyesexternalRef or org_ id.
streamyesLowercase, dot-separated. One chain per organization and stream.
daynoYYYY-MM-DD (UTC). Defaults to today. Days go in order: never before the latest recorded day.
stateone ofThe full state as JSON; fingerprinted, not kept.
digestone ofOr its sha256, computed on your side.
metricsyes1 to 8 of { name, value, check? }. Names are public; values are whole numbers from 0 to 2^64-1 (use cents for money) and stay private.
checkno{ op: eq | lte | gte, value }. Public. The proof shows whether it holds.

The response comes back once the proof is made (under a second) with 201, the commitment, the check results, and an opening.salt that is returned only this once (see opening a commitment). zkVerify verification and the Horizen anchor follow within a minute or two; the snapshot.verified and snapshot.anchored webhooks tell you when. Sending the same day again with the same state returns the existing record; a different state for a recorded day is refused with 409, because days can't be changed.

Choosing metrics

Pick numbers an auditor would ask about, and checks that state your commitments: unreconciled_payouts eq 0, critical_findings eq 0, mfa_coverage_pct gte 95, days_since_last_backup lte 1. Metrics without a check are committed too, so they can be opened later, but verification pages show no result for them.

Proof Cards

Credentials your organizations issue to people.

Each organization can issue Proof Cards (W3C Verifiable Credentials) to the people it works with: access granted, a membership, a completed training, an approved supplier. The card is signed with the organization's own key, and its issuance, its SD-JWT copy and every later suspension, reinstatement or revocation are anchored as the organization's events, one transaction each from its wallet. The recipient gets an email to accept the card into their wallet; send notify: false to skip it.

Send the same body as POST /api/v1/credentials (template, title, recipient, fields), with organization naming the issuing organization. Change a card's status with POST /api/v1/credentials/{id}/status and { action: "suspend" | "reinstate" | "revoke" }.

issue a Proof Card
await fetch("https://attest.datahubz.com/api/v1/credentials", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.ATTEST_INTEGRATION_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    organization: "company:118",
    template: "membership",
    title: "Owner portal access",
    recipient: { email: "user@domain.com", name: "Mary Ruiz" },
    fields: { memberSince: "2026-09-29" },
  }),
});

Certificates

Certify an organization, published on Attest.

A platform issues certificates about the organizations it serves through the same key. Each certificate is issued by the organization DataHubz assigns to your platform, is signed by the people named on it with their passkeys, then by the issuer as a W3C Verifiable Credential, and is anchored on Horizen. Its page, PDF and verification live on Attest only: link to the url it returns rather than publishing the certificate yourself. It also appears on the organization's transparency page.

Send the same body as POST /api/v1/certificates, with organization (the organization's id or your externalRef) in place of orgId. With findingsClosure, the certificate carries a zero-knowledge proof, verified on zkVerify, that every finding in the committed set is closed; the opening salt is returned once.

issue a certificate
const res = await fetch("https://attest.datahubz.com/api/v1/certificates", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.ATTEST_INTEGRATION_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    organization: "client:4821",
    title: "Security assessment",
    subjectName: "Mary Ruiz Consulting",
    statement: "Every finding from the assessment was remediated and verified at retest.",
    asOf: "2026-10-15T00:00:00Z",
    sections: [{ heading: "Scope", items: [{ label: "Web application", result: "Passed" }] }],
    signers: [{ email: "user@domain.com", name: "Mary Ruiz", role: "Lead assessor" }],
  }),
});
const cert = await res.json(); // cert.url: the public certificate, cert.signUrl: where the signers sign

Webhooks

Know when a record is final.

Set a webhook URL in Dashboard → Integrations and Attest will POST to it when a record reaches a final state. Each call is signed; deliveries that fail are retried for about a day (1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours). Receivers should be idempotent on the payload's id.

TypeWhen
event.anchoredAn event's transaction is confirmed on Horizen.
event.failedAn event could not be anchored after retries.
snapshot.anchoredA daily state's commitment is confirmed on Horizen.
snapshot.verifiedA daily state's proof is verified on zkVerify.
snapshot.failedzkVerify rejected a daily state's proof.
pingSent from the dashboard to test your endpoint.

The Attest-Signature header is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> with your signing secret. Verify it against the raw body, before parsing:

verify a webhook
import { Attest } from "@datahubz/attest";

export async function POST(req: Request) {
  const raw = await req.text();
  const event = await Attest.verifyWebhook(raw, req.headers.get("attest-signature"), process.env.ATTEST_WEBHOOK_SECRET!);
  if (event.type === "snapshot.verified") {
    // event.data is the snapshot, with proof.zkVerify.txHash
  }
  return new Response(null, { status: 204 });
}

Verification

Anyone can check, without trusting Attest.

Every record has a page at /verify/<id> (the verifyUrl in each response) that checks it live: it reads the transaction back from Horizen, confirms the anchored value and that the sender is the organization's wallet, and, for daily states, verifies the zero-knowledge proof again and links to zkVerify. No account is needed.

In your dashboard, Transparency shows each of your organizations: its wallet, its daily chain with the result of every check, its events, and links to every transaction on Horizen and zkVerify.

Without Attest at all, anyone can ask the registry on Horizen directly, and verify a daily proof with snarkjs:

independent verification
# Is this value anchored, when, and by which wallet?
cast call 0x0C6a7918e2f8C28C4bb5Cf8d0f4CE27e9B30fa2A \
  "isAnchored(bytes32)(bool,uint256,address)" 0x<anchor value> \
  --rpc-url https://horizen.calderachain.xyz/http

# Verify a daily state's proof (proof and publicSignals from GET /api/v1/snapshots/{id})
curl -sO https://attest.datahubz.com/zk/state_attestation_vk.json
snarkjs groth16 verify state_attestation_vk.json public.json proof.json

The circuit is state_attestation.circom. Its keys come from the Privacy and Scaling Explorations Perpetual Powers of Tau ceremony (80 contributions), and its verification key is registered on zkVerify.

Opening a commitment

Show an auditor the numbers, and prove they are the ones committed.

Verification pages show check results, never values. When an auditor needs the values for a day, you give them the metric values and the opening.salt returned when the day was recorded (store it with your record of the day). The auditor recomputes the commitment and compares it with the value anchored on Horizen:

commitment
// BN254 field, Poseidon as in circomlib (e.g. the poseidon-lite package)
const p = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
const f = (hex) => BigInt("0x" + hex) % p;
const org = f(sha256("attest.org|" + organizationId));
const stream = f(sha256("attest.stream|" + streamName));
const day = Math.floor(Date.parse(dayUtc) / 86400000);
const values = [...metricValues, 0, 0, …].slice(0, 8);   // in the order they were sent, zero-padded
const commitment = poseidon7([org, stream, day, f(stateDigest), previousCommitment, poseidon8(values), salt]);

If the values or the salt are off by anything at all, the commitment won't match.

API reference

Endpoints.

Base URL https://attest.datahubz.com. Authenticate with Authorization: Bearer ik_live_…. Requests and responses are JSON. Lists are newest first, with limit (up to 100) and a cursor from the previous page's nextCursor.

MethodPathDoes
GET/api/v1/partnerThe platform this key belongs to. A quick key check.
POST/api/v1/organizationsRegister or rename an organization (idempotent on externalRef). 201 new, 200 existing.
GET/api/v1/organizationsYour organizations.
GET/api/v1/organizations/{ref}One organization, by org_ id or externalRef.
PATCH/api/v1/organizations/{ref}Exclude an organization or bring it back: { excluded }.
POST/api/v1/eventsRecord an event. 202, anchored moments later.
GET/api/v1/eventsYour events. Filters: organization, type, status.
GET/api/v1/events/{id}One event, with its anchor once confirmed.
POST/api/v1/snapshotsRecord a day's state. 201 with the proof; verified and anchored within minutes.
GET/api/v1/snapshotsYour daily states. Filters: organization, stream.
GET/api/v1/snapshots/{id}One daily state, with its proof and zkVerify transaction.
POST/api/v1/credentialsIssue a Proof Card from one of your organizations. 201, anchored moments later.
GET/api/v1/credentialsCards your organizations issued. Filter: organization.
GET/api/v1/credentials/{id}One card, with a verifiable copy and its SD-JWT.
POST/api/v1/credentials/{id}/statusSuspend, reinstate or revoke a card.
POST/api/v1/certificatesCreate a certificate about one of your organizations. 201, awaiting its signers.
GET/api/v1/certificatesYour platform's certificates. Filter: organization.
GET/api/v1/certificates/{id}One certificate, with its status, PDF and anchor.
POST/api/v1/verifyPublic, no key: { eventId } or { snapshotId } returns the live verification.

Errors and limits

Plain errors, safe retries.

Errors are { "error": { "code", "message" } } with a meaningful status: 400 invalid_request, 401 unauthorized, 403 partner_disabled, 404 not_found, 409 conflict, 409 organization_excluded, 429 rate_limited (with Retry-After), 500 internal_error. Retry 429 and 5xx with backoff; every write is idempotent, so retries never duplicate records.

LimitValue
Requests per key600 a minute
Request body256 KB
Metrics per daily state8, whole numbers from 0 to 2^64-1
Metadata20 keys, values up to 200 characters
Daily statesOne per organization, stream and day, in order
Network costsPaid by the Attest treasury for enabled platforms

TypeScript client

One package, no dependencies.

install
npm install @datahubz/attest

@datahubz/attest has no dependencies and runs on Node 18+, Deno, Bun and edge runtimes. It retries network errors, 429 and 5xx with backoff, adds idempotency keys, computes the same digests as Attest (digest(json) for data, digestBytes(bytes) for files), polls with events.waitForAnchor, and verifies webhooks with Attest.verifyWebhook. It is also a single file, if you would rather vendor it: attest.ts.

keep data on your side
import { Attest, digest, digestBytes } from "@datahubz/attest";

// Only the fingerprint leaves your servers:
await attest.events.record({ organization: "hotel-42", type: "document.signed", digest: await digestBytes(pdfBytes) });
await attest.snapshots.record({ organization: "hotel-42", stream: "hotel.daily", digest: await digest(state), metrics });

Dashboard

Keys, webhooks and organizations.

Dashboard → Integrations is where a platform's owner creates and revokes integration keys, sets the webhook URL (and sends a signed test ping), and sees every organization with its wallet, its events and its latest daily proof, with a button to exclude or include it. Dashboard → Transparency shows the same organizations in detail. DataHubz enables Integrations for each platform and pays its organizations' network costs while the integration is sponsored.

Ready to integrate? Create a key and send your first event.

Integrations →