On this page
API reference
Everything in the dashboard, as an API.
Create receipts for anything, send end-to-end encrypted agreements, prove facts in zero knowledge, issue Proof Cards, seal documents until a deadline or an approval, collect anonymous verified responses, and verify all of it. Attest handles the wallets, the gas, and the blockchains behind every call. You send JSON. What only a person's passkey can do is listed under Dashboard-only.
Base URL
https://attest.datahubz.com/api/v1Format
JSON in, JSON out. Times are ISO 8601, UTC.
Authentication
Create an API key in the dashboard (Build, then Developers, then New key) and send it as a bearer token. A key acts as the account that created it. Verification endpoints, and the others marked Public, need no key. To try any endpoint with your key before writing code, open the API playground.
Authorization: Bearer ak_live_...Errors and limits
Errors use HTTP status codes and a consistent body. The free plan includes 10 records a month: receipts, agreements, proofs, and Proof Cards you create, and sealed calls, releases, vaults, and forms. Creating a receipt, agreement, proof, or Proof Card returns your allowance in X-Attest-Quota-* headers. Signing, reading, and verifying never count.
{ "error": { "code": "quota_exceeded", "message": "Free plan limit of 10 records this month reached." } }400 bad_request | The request is missing or has an invalid field. | |
401 unauthorized | No valid API key. | |
402 quota_exceeded | The free plan's monthly allowance is used up. | |
403 / 404 | Not yours, private, or doesn't exist. | |
409 | Not possible in the current state (for example, not your turn to sign). |
Receipts
A receipt proves that content with a given SHA-256 fingerprint existed, unchanged, at a point in time. Hash locally and send only the fingerprint: your content never has to leave your systems. Receipts are batched and anchored on a public blockchain within minutes.
sha256 | string | SHA-256 of the content, 64 hex characters. Preferred. |
content | string | Alternatively, text to hash server-side (and discard). |
kind | string | One of: document, image, video, audio, event, agent_action, code_artifact, custom. Default document. |
claim | string | A short human statement, e.g. “Q3 report, final”. |
metadata | object | Any JSON you want recorded with it. |
supersedes | string | The receipt ID this one corrects. Nothing is ever deleted. |
curl -X POST https://attest.datahubz.com/api/v1/receipts \
-H "Authorization: Bearer ak_live_..." -H "Content-Type: application/json" \
-d '{
"kind": "agent_action",
"sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"claim": "Agent invoice-bot filed the Q3 VAT return",
"metadata": { "agentId": "invoice-bot", "authorizedBy": "cfo@acme.com" }
}'{ "id": "rcpt_01JD...", "kind": "agent_action", "status": "pending", "anchor": null, ... }limit up to 200). Each includes its anchor once confirmed: chain, transaction, block, and an explorer link.Verify (public)
Anyone can verify, with no account and no key. The same checks power the verifier at attest.datahubz.com/verify. New to Attest? Start with the introduction.
receiptId | string | Verify a receipt: inclusion proof, anchor, and any zero-knowledge claim. |
sha256 | string | Find the receipt for a fingerprint, plus signed agreements (with public certificates) whose document has it. |
agreementId | string | A signed agreement's signers and anchor, if its certificate is public. |
proofId | string | A zero-knowledge agreement proof: validity, identities, anchor, zkVerify. |
credential | object | string | A W3C Verifiable Credential (object), or an SD-JWT VC (string): signature, issuer, revocation and suspension status, validity dates, and for Proof Cards the issuance anchor. Other issuers too: did:web and did:key, with ecdsa-sd-2023, ecdsa-rdfc-2019, eddsa-rdfc-2022, or Ed25519Signature2020 proofs. |
shareId | string | A Proof Card someone shared (shr_…), checked the same way, plus the holder's passkey approval of that exact copy. |
curl -X POST https://attest.datahubz.com/api/v1/verify -H "Content-Type: application/json" \
-d '{ "sha256": "'"$(shasum -a 256 contract.pdf | cut -d' ' -f1)"'" }'Agreements
Agreements are end-to-end encrypted: you encrypt the document before sending it, so Attest only ever stores ciphertext. Each party has their own key pair, set up once in the dashboard with a passkey. Signers are emailed a signing link; when everyone has signed, the agreement is anchored on a public blockchain from its creator's wallet.
title | string | Required. |
documentSha256 | string | SHA-256 of the plaintext document. Required; it's what gets anchored. |
documentName | string | File name, e.g. nda.pdf. |
document | object | { ciphertext, iv, contentType, size }, AES-256-GCM, base64. |
creatorWrappedDek | string | The document key wrapped to your public key. Required. |
signerWrappedDeks | object | { email: wrappedKey } for signers who already have a key. Others are granted later. |
signers | array | [{ email, name? }], up to 20. |
includeSelf | boolean | Add yourself as a signer. |
selfPosition | string | first (default) or last, when signing in order. |
sequential | boolean | Sign one at a time, in order. |
publicCertificate | boolean | Default true. False keeps the certificate private; outsiders verify through zero-knowledge proofs. |
incoming (awaiting your signature) and signed (signed by you).Encrypting a document
Generate a random 256-bit document key, encrypt the file with AES-GCM, and wrap the key to each party's RSA-OAEP (SHA-256) public key. Fetch public keys with POST /api/v1/keys/public. This runs anywhere WebCrypto does: Node 20+, Deno, Bun, or the browser.
{ "emails": [...] } returns { "publicKeys": { email: spkiBase64 } }. People without a key yet are simply absent; grant them access after they set one up.import { readFile } from "node:fs/promises";
import { createHash, webcrypto as crypto } from "node:crypto";
const API = "https://attest.datahubz.com/api/v1";
const KEY = process.env.ATTEST_API_KEY;
const api = async (method, path, body) => {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(JSON.stringify(await res.json()));
return res.json();
};
const b64 = (buf) => Buffer.from(buf).toString("base64");
const me = "you@company.com";
const signers = ["mary@acme.com"];
const doc = await readFile("nda.pdf");
// 1. Public keys (yours is required; signers without one are granted later)
const { publicKeys } = await api("POST", "/keys/public", { emails: [me, ...signers] });
// 2. Encrypt the document with a fresh key
const dek = crypto.getRandomValues(new Uint8Array(32));
const iv = crypto.getRandomValues(new Uint8Array(12));
const aes = await crypto.subtle.importKey("raw", dek, "AES-GCM", false, ["encrypt"]);
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, doc);
// 3. Wrap the key to each party
const wrap = async (spki) => {
const pub = await crypto.subtle.importKey("spki", Buffer.from(spki, "base64"),
{ name: "RSA-OAEP", hash: "SHA-256" }, false, ["encrypt"]);
return b64(await crypto.subtle.encrypt({ name: "RSA-OAEP" }, pub, dek));
};
const signerWrappedDeks = {};
for (const email of signers) if (publicKeys[email]) signerWrappedDeks[email] = await wrap(publicKeys[email]);
// 4. Create it
const agreement = await api("POST", "/agreements", {
title: "Mutual NDA, Acme & Co",
documentName: "nda.pdf",
documentSha256: createHash("sha256").update(doc).digest("hex"),
document: { ciphertext: b64(ciphertext), iv: b64(iv), contentType: "application/pdf", size: doc.length },
creatorWrappedDek: await wrap(publicKeys[me]),
signerWrappedDeks,
signers: signers.map((email) => ({ email })),
includeSelf: true,
sequential: true,
});
// 5. Email the signing links
await api("POST", `/agreements/${agreement.id}/send`);Agreement actions
/api/v1/agreements/{id}/consentAPI key{ "name": "Your Name" }. Returns the exact consent statement, a consentToken (valid 5 minutes), the WebAuthn challenge (SHA-256 of the statement), and your signing passkeys as allowCredentials./api/v1/agreements/{id}/signAPI key{ "consentToken", "assertion": { id, response: { authenticatorData, clientDataJSON, signature } } }, the passkey assertion over that challenge, base64url. Attest verifies it against your passkey's public key before recording it. A signature always comes from a person's passkey, so this step runs where the passkey is: the dashboard, or your own WebAuthn integration. Other parties sign through their emailed link./api/v1/agreements/{id}/grantAPI key{ "grants": { email: wrappedKey } }.ciphertext, iv, wrappedDek, and documentSha256 to check the result against.{ "publicCertificate": true | false }. Private agreements are visible only to their parties; others verify them through zero-knowledge proofs.{ "reason": "..." }.Zero-knowledge proofs
Prove a fact about a completed agreement you're a party to, revealing only that fact. Proofs are Groth16, checked against an anchored record of all agreements, and verified independently on zkVerify.
agreementId | string | A completed agreement you're a party to. |
circuit | string | agreement_with_party (default): you and a named counterparty signed it, on a date. agreement_membership: you're a party to it, on a date. |
counterpartyEmail | string | Required for agreement_with_party: another signer to reveal. |
{ "id": "azk_01M3..." }https://attest.datahubz.com/claim/{id} with anyone.Compliance proofs
Compliance proofs run on the security-scan Groth16 circuit from Hubz VCE, a DataHubz project. Each proof is checked by Attest and verified on zkVerify, and its receipt is anchored on Horizen.
predicate | string | zero_critical, zero_high, zero_both, score_threshold, or full_compliance. |
scan | object | { criticalCount, highCount, mediumCount, lowCount, infoCount, securityScore, scanners[], minSecurityScore? } |
claim | string | Optional label for the receipt. |
Proof Cards
Issue credentials people carry and prove anywhere: training, supplier approvals, memberships, employment, or your own fields. Each card is a W3C Verifiable Credential 2.0, signed by your organization with a Data Integrity proof (ecdsa-sd-2023), so the holder can reveal only the fields they choose and the signature still verifies. Revocation and suspension use W3C Bitstring Status Lists. Every issuance and every status-list change is anchored on Horizen, so an issuer can't backdate a card or hide a revocation. Issuing uses one record of your allowance; status changes, reading, and verifying don't.
Training cards are issued in the 1EdTech Open Badges 3.0 format (OpenBadgeCredential), so learning platforms and badge wallets can read them. You send the same fields; Attest maps completedOn to activityEndDate, hours to creditsEarned, result to a Result, and the title to the Achievement, which is always disclosed.
template | string | training, supplier, membership, employment, or custom. GET /api/v1/credential-templates lists their fields. |
title | string | Required. What the card is: the course, the approval scope, the membership, the role. |
recipient | object | { email, name? }. The email delivers it; it's only put in the credential with includeEmail. |
fields | object | The template's fields, e.g. { completedOn: "2026-09-20", hours: 8 }. For custom, your own labels. |
validUntil | string | ISO date. Omit for the template's default (a year for most); null for no expiry. |
validFrom | string | ISO date. Default now. |
description | string | Optional, always disclosed. |
includeEmail | boolean | Default false. |
orgId | string | Which of your organizations issues it. Default your first. |
notify | boolean | Default true: email the recipient. |
curl -X POST https://attest.datahubz.com/api/v1/credentials \
-H "Authorization: Bearer ak_live_..." -H "Content-Type: application/json" \
-d '{
"template": "training",
"title": "Forklift operator certification",
"recipient": { "email": "mary@acme.com", "name": "Mary Ruiz" },
"fields": { "completedOn": "2026-09-18", "hours": 16, "result": "Passed" }
}'{ "id": "cred_01M3...", "credentialId": "urn:uuid:...", "state": "active",
"issuer": { "name": "Acme Training", "did": "did:web:attest.datahubz.com:orgs:acme-training" },
"claimUrl": "https://attest.datahubz.com/cards/claim/...", "receiptId": "rcpt_01M3...", ... }limit up to 100).verifiableCredential: a verifiable copy disclosing every field.{ "action": "suspend" | "reinstate" | "revoke", "reason"?: "...", "message"?: "..." }. Publishes a new, anchored version of your status list and emails the holder. reason stays private to you; message is included in the email. Revocation is permanent.Checking a card without Attest. Everything a verifier needs is public and standard, so any software supporting these W3C specifications can check a card on its own:
/orgs/{slug}/did.json | did:web | The issuer's DID document and P-256 public key (Multikey). |
/orgs/{slug}/status/{n}/{purpose} | W3C | The signed Bitstring Status List credential (revocation or suspension). |
…/{purpose}/history | JSON | Every published version of that list, with its SHA-256 and anchor. |
/contexts/cards/v1 | JSON-LD | The context defining Proof Card types and fields. |
Wallets: OpenID4VCI and OpenID4VP
Every card exists in two formats: an IETF SD-JWT VC (dc+sd-jwt, ES256, issuer did:web, revocation through an IETF Token Status List) following the FIDES DIIP v5 interoperability profile, and the W3C Verifiable Credential described above. Wallet apps pick the format they read.
Proof Cards work with wallet apps through the OpenID Foundation's standards. OpenID4VCI 1.0 delivers a card to a wallet: the holder clicks “Add to a wallet app” and gets a one-time QR code and a 6-digit PIN (pre-authorized code flow). A wallet that proves its key (a jwt proof, did:key or did:jwk) receives a copy bound to that key (cnf.kid for SD-JWT, credentialSubject.id for W3C); otherwise an unbound one. The authorization code flow is supported too, with pushed authorization requests and PKCE S256. OpenID4VP 1.0 lets you ask someone to present a card: wallet apps get a request object signed by Attest's did:web (passed by reference, request_uri_method=get), with a DCQL query accepting either format and direct_post. A presentation is accepted only with holder binding: an SD-JWT key binding JWT for this request, a W3C presentation signed by the key the card is bound to, or approval with the holder's passkey in Attest.
/.well-known/openid-credential-issuer | OID4VCI | Credential issuer metadata: one ldp_vc configuration per card type. |
/.well-known/oauth-authorization-server | OAuth | Token endpoint for the pre-authorized code grant. |
/api/oid4vci/par · /oid4vci/authorize | OAuth | Pushed authorization request, then the holder approves at Attest (authorization code flow). |
/api/oid4vci/token · /nonce · /credential | OID4VCI | Exchange the code (and PIN, or PKCE verifier); get a nonce; collect the credential. |
/orgs/{slug}/statuslists/{n} | IETF | Token Status List for SD-JWT cards (2 bits: valid, revoked, suspended). |
/vct/{type} | SD-JWT VC | Type metadata: how wallets name and display each card type. |
/.well-known/did.json | did:web | Attest's verifier identity, which signs presentation requests. |
/api/oid4vp/response/{id} | OID4VP | direct_post endpoint for a presentation request. |
Check the card, not the screen. A wallet app's display, a screenshot, or a printout can't show that a card is still valid, and some wallet apps don't show a suspension or revocation to their user. Verify by requesting the card (below) or opening its share link: every check reads the issuer's status list at that moment. When an issuer suspends, reinstates, or revokes a card, Attest emails the holder.
purpose | string | Required. Shown to the holder before they decide. |
credentialType | string | OpenBadgeCredential, SupplierApprovalCredential, MembershipCredential, EmploymentCredential, or AttestCredential. Omit for any. |
fields | string[] | credentialSubject properties to reveal, e.g. ["name", "activityEndDate"]. |
issuerDid | string | Accept only cards from this issuer. |
{ "id": "vpr_01M3...", "status": "waiting",
"walletLink": "openid4vp://?client_id=redirect_uri%3A...&dcql_query=...",
"attestLink": "https://attest.datahubz.com/present?...", "expiresAt": "..." }status is waiting, verified, rejected, or expired. Once answered, result has the credential's verification, the holder binding, and whether it meets your request.Sealed submissions
Collect proposals, bids, or applications that no one can open before a deadline. Each submission is encrypted in the submitter's browser with a fresh AES-256 key; that key is wrapped to every reviewer's public key, and the wrapped keys are time-locked with drand (quicknet, the League of Entropy) to the first round published at or after the deadline. Before then, no one can open a submission: the key that unlocks the time lock is produced by a threshold of independent drand operators only at that moment. After it, only the reviewers can, since Attest never holds their private keys. The call's terms (deadline, round, reviewers) are anchored when it's created, and every submission's sealed bytes when it arrives. Replacements are allowed before the deadline, and every version stays on record.
A key id, here and in the sections below, is the SHA-256 (hex) of the person's public key as its base64 SPKI text. Get public keys with POST /api/v1/keys/public.
{ "title", "description"?, "deadline": ISO, "reviewerIds"?: [...], "orgId"? }. The deadline must be 5 minutes to a year away. Reviewers are you plus members of your organization who have an encryption key (GET /api/v1/sealed/reviewers lists them with their ids). Returns { id, url, deadline, drandRound, termsReceiptId }. Uses one record.drand.chain, drand.round), the reviewers' public keys and key ids to seal to, and every submission's receipt (no names)./api/v1/sealed/{id}/submissionsAPI key{ ciphertext, iv, metaCiphertext, metaIv, sealedKey }, where sealedKey is a tlock (age) ciphertext of { reviewerKeyId: wrappedKey } for the call's round, up to 3 MB. Keys sealed to any other round are refused. Submitting again replaces your earlier version for the reviewers./api/v1/sealed/{id}/submissionsAPI key/api/v1/sealed/{id}/openedAPI keyApproval release
A document that opens for chosen recipients only after k of n approvers approve it, and optionally not before a date. It's encrypted in the creator's browser with a key K = K1 xor K2. K1 is split with Shamir's secret sharing into one share per approver (any k rebuild it; fewer reveal nothing), and each share is encrypted to that approver's public key. K2 is time-locked with drand to the “not before” date, or zero without one. Approving happens in the approver's browser: their share is decrypted with their key, re-encrypted to each recipient, and the decision is signed with their passkey. Recipients rebuild K from k shares (plus drand's key for the date). Attest only ever holds ciphertext. The terms (approvers, threshold, recipients, date, document fingerprint) are anchored at creation, and every approval or decline when it's made.
{ "emails": [...] } returns { found: [{ email, publicKey, keyId }], missing: [...] } for up to 25 people. Everyone taking part needs an Attest account with an encryption key. Because this shows whether an address has a key, each account can look up at most 60 new addresses an hour and 300 a day (repeat lookups are free); beyond that it returns 429 rate_limited./api/v1/releasesAPI key{ title, description?, ciphertext, iv, metaCiphertext, metaIv, threshold, approvers: [{ email, keyId, share }], recipients: [{ email, keyId }], notBefore?, timeKey? }. 1 to 10 approvers and recipients, up to 3 MB. timeKey is a tlock ciphertext of K2 for the drand round of notBefore (at least 5 minutes away). Approvers are emailed. Returns { id, url, termsReceiptId }. Uses one record./api/v1/releases/{id}/cancelAPI keyApproving and declining are signed with the approver's passkey, so they happen in the dashboard.
Continuity vault
Documents and notes kept sealed until the moment others need them: handovers, account access, instructions. They're released to chosen beneficiaries only if the owner stops checking in and k of n trustees confirm. Each item is encrypted in the owner's browser with its own key; those keys are wrapped with a vault key V, which is wrapped to the owner and split among the trustees with Shamir's secret sharing. Once the owner misses a check-in plus the grace period, each trustee can confirm with their passkey, re-encrypting their share to the beneficiaries in their own browser. A check-in cancels outstanding confirmations, and locks a released vault again unless a beneficiary has already received it. Changing trustees or beneficiaries re-keys the vault. The terms, every item and every confirmation are anchored.
What's guaranteed by cryptography: fewer than k trustees can't open it, and neither can Attest. What's Attest's rule: the check-in timer, which decides when trustees may confirm.
/api/v1/vaultsAPI key{ title, checkInDays: 7–365, graceDays: 1–60, threshold, trustees: [{ email, keyId, share }], beneficiaries: [{ email, keyId }], ownerKey, ownerKeyId, contents: [{ ciphertext, iv, metaCiphertext, metaIv, wrappedDek }] }. At least one item in contents: a vault can't be empty. You can't be your own trustee or beneficiary. Trustees and beneficiaries are emailed. Uses one record.?open=1 after release to receive the items and the trustees' shares (from then on, a check-in can't lock it again)./api/v1/vaults/{id}API keyitems: [{ id, wrappedDek }] re-wrapping every item's key. Outstanding confirmations are cancelled, and it counts as a check-in./api/v1/vaults/{id}API key/api/v1/vaults/{id}/itemsAPI key{ ciphertext, iv, metaCiphertext, metaIv, wrappedDek }, up to 3 MB each and 20 per vault./api/v1/vaults/{id}/items/{itemId}API keyDELETE removes it; the last item can't be removed.Check-ins and trustee confirmations are signed with a passkey, so they happen in the dashboard.
Verified Forms
Anonymous responses from a known group, one per member: team pulses, committee votes, member and customer feedback. Built on Semaphore v4 (Groth16 on BN254). Each invited member joins by creating a per-form identity in their browser; only its commitment joins the group (a Merkle tree). A response carries a proof that its author is in the group, bound to the exact answers (the proof's message is sha256(canonicalJson({ form, answers }))) and to the form (its scope). The nullifier is the same for any two responses from one member, so a second response is refused, without revealing who either came from. Responses are submitted without a session. Every proof is verified by Attest and also on zkVerify.
The terms are anchored at creation; with “join first, then answer”, the group is fixed and anchored when answering opens; the full set of responses is anchored at close. Answering needs at least 3 members in the group. The evidence file contains everything needed to re-check the results: the group's commitments, and every response with its proof (depth 16).
/api/v1/formsAPI key{ title, description?, questions: [{ type: single|multiple|rating|text, label, options?, required }], emails: [...], includeOrg?, card?: { template, title }, twoPhase, opensAt?, closesAt, visibility: organizer|participants|public }. Up to 30 questions; single and multiple choice take 2 to 20 options; ratings are 1 to 5. With twoPhase, joining closes at opensAt (at least 10 minutes away). With card, holders of an active Proof Card of that type from your organization are eligible, checked when they join (a revoked or suspended card can't join; a card received before joining closes can). At least 3 people. Uses one record.cardKinds: the card types your organization has issued, for card eligibility.DELETE cancels it (the organizer, before its results are anchored)./api/v1/forms/{id}/openAPI key/api/v1/forms/{id}/closeAPI key/api/v1/forms/{id}/responsesPublic{ answers, proof }, where proof is a Semaphore v4 proof (depth 16) for the form's scope. No session is read; the proof is the only credential.Joining happens in the dashboard, where the member's identity is created. What's guaranteed by cryptography: a response can't be linked to a member from the data, by the organizer or anyone checking the evidence, and nobody can answer twice or without being on the list. What remains: Attest's servers see when requests arrive; we don't link a member's visit to a response, and written answers can reveal their author by what they say.
Dashboard-only, by design
- Keys and passkeys. Your private key is created and unlocked in your browser with a passkey, and never reaches our servers. Set it up once on the Security page.
- Opening documents. Decryption happens where your key is. Through the API you receive ciphertext and decrypt it yourself.
- Anything signed with a passkey. Signing agreements (other than your own WebAuthn integration, above), approving or declining a release, checking in on a vault, confirming as a trustee, and approving a Proof Card share are made by the person's passkey (Face ID, Touch ID, Windows Hello), so no API key alone can do them on anyone's behalf. Each is verifiable by anyone:
POST /api/v1/verifywith anagreementIdreturns every signer's evidence and our check of it. - Holding cards. Accepting a Proof Card, sharing it, and adding it to a wallet app happen in the holder's dashboard.
- Joining a Verified Form. The member's identity for the form is created in their browser.
- Organizations, billing, and API keys. Managed in the dashboard.