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.
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ú" }'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 }
}'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:
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_…).
{
"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.
| Field | Required | What it is |
|---|---|---|
organization | yes | externalRef or org_ id. |
type | yes | What happened: lowercase, dot-separated, e.g. statement.closed, evidence.approved. |
data | one of | Any JSON. Attest fingerprints its canonical form (keys sorted) and doesn't keep it. |
digest | one of | Or send the sha256 yourself (64 hex characters), and the data never leaves your servers. |
subject | no | Your id for what it's about, e.g. statement-8841. Up to 200 characters. |
occurredAt | no | ISO 8601. Defaults to now. |
metadata | no | Up 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.
{
"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:
- 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. - 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.
- Verifies the proof on zkVerify, where validators check it and record the result.
- 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.
| Field | Required | What it is |
|---|---|---|
organization | yes | externalRef or org_ id. |
stream | yes | Lowercase, dot-separated. One chain per organization and stream. |
day | no | YYYY-MM-DD (UTC). Defaults to today. Days go in order: never before the latest recorded day. |
state | one of | The full state as JSON; fingerprinted, not kept. |
digest | one of | Or its sha256, computed on your side. |
metrics | yes | 1 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. |
check | no | { 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" }.
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.
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 signWebhooks
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.
| Type | When |
|---|---|
event.anchored | An event's transaction is confirmed on Horizen. |
event.failed | An event could not be anchored after retries. |
snapshot.anchored | A daily state's commitment is confirmed on Horizen. |
snapshot.verified | A daily state's proof is verified on zkVerify. |
snapshot.failed | zkVerify rejected a daily state's proof. |
ping | Sent 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:
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:
# 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.jsonThe 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:
// 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.
| Method | Path | Does |
|---|---|---|
| GET | /api/v1/partner | The platform this key belongs to. A quick key check. |
| POST | /api/v1/organizations | Register or rename an organization (idempotent on externalRef). 201 new, 200 existing. |
| GET | /api/v1/organizations | Your 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/events | Record an event. 202, anchored moments later. |
| GET | /api/v1/events | Your events. Filters: organization, type, status. |
| GET | /api/v1/events/{id} | One event, with its anchor once confirmed. |
| POST | /api/v1/snapshots | Record a day's state. 201 with the proof; verified and anchored within minutes. |
| GET | /api/v1/snapshots | Your daily states. Filters: organization, stream. |
| GET | /api/v1/snapshots/{id} | One daily state, with its proof and zkVerify transaction. |
| POST | /api/v1/credentials | Issue a Proof Card from one of your organizations. 201, anchored moments later. |
| GET | /api/v1/credentials | Cards 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}/status | Suspend, reinstate or revoke a card. |
| POST | /api/v1/certificates | Create a certificate about one of your organizations. 201, awaiting its signers. |
| GET | /api/v1/certificates | Your platform's certificates. Filter: organization. |
| GET | /api/v1/certificates/{id} | One certificate, with its status, PDF and anchor. |
| POST | /api/v1/verify | Public, 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.
| Limit | Value |
|---|---|
| Requests per key | 600 a minute |
| Request body | 256 KB |
| Metrics per daily state | 8, whole numbers from 0 to 2^64-1 |
| Metadata | 20 keys, values up to 200 characters |
| Daily states | One per organization, stream and day, in order |
| Network costs | Paid by the Attest treasury for enabled platforms |
TypeScript client
One package, no dependencies.
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.
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 →