BitGraph Protocol API
REST API for fusing and committing artifacts, looking up proofs by digest, and verifying proofs. The commit path runs inside an AWS Nitro Enclave.
https://nitro.occproof.comThe enclave host. Routes under /api/ are served by the site at https://bitgraph.ing.
Authentication
Authentication is optional. If the server is configured with API keys, include a Bearer token:
Authorization: Bearer <your-api-key>
If no API keys are configured on the server, all endpoints are open. The public demo endpoint does not require authentication.
Endpoints
/commitRecord existing bytes: commit one or more artifact digests. For each digest, the enclave allocates a causal slot (nonce + counter), then commits the artifact against that slot. Returns a complete BitGraph proof for each digest. For a fused artifact use /api/fuse/allocate and /api/fuse/commit instead. Requires API key if configured.
Request body
{
"digests": [
{
"digestB64": "jYl9NHJP0VcRVh6OMEIU5VAGva6cu5kdrnPrlNr/RnU=",
"hashAlg": "sha256"
}
],
"attribution": { // optional - signed creator metadata
"name": "Jane Doe",
"title": "Sunset at Malibu",
"message": "Original RAW capture"
},
"metadata": { // optional, advisory (NOT signed)
"source": "my-app",
"fileName": "document.pdf"
}
}Response (200)
[
{
"version": "bitgraph/1",
"artifact": {
"hashAlg": "sha256",
"digestB64": "jYl9NHJP0VcRVh6OMEIU5VAGva6cu5kdrnPrlNr/RnU="
},
"commit": {
"nonceB64": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"counter": "278",
"slotCounter": "277",
"slotHashB64": "...",
"time": 1741496392841,
"epochId": "a1b2c3d4e5f6..."
},
"signer": {
"publicKeyB64": "...",
"signatureB64": "..."
},
"environment": {
"enforcement": "measured-tee",
"measurement": "ac813febd1ac4261...",
"attestation": {
"format": "aws-nitro",
"reportB64": "..."
}
},
"slotAllocation": {
"version": "bitgraph/slot/1",
"nonceB64": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"counter": "277",
"time": 1741496392800,
"epochId": "a1b2c3d4e5f6...",
"publicKeyB64": "...",
"signatureB64": "..."
},
"timestamps": {
"artifact": {
"authority": "http://freetsa.org/tsr",
"time": "2026-03-07T12:00:00Z",
"digestAlg": "sha256",
"digestB64": "...",
"tokenB64": "..."
}
}
}
]Example: curl
DIGEST=$(openssl dgst -sha256 -binary myfile.pdf | base64)
curl -X POST https://nitro.occproof.com/commit \
-H "Content-Type: application/json" \
-d '{
"digests": [{
"digestB64": "'$DIGEST'",
"hashAlg": "sha256"
}]
}'Example: TypeScript
const bytes = new Uint8Array(await file.arrayBuffer());
const hashBuf = await crypto.subtle.digest("SHA-256", bytes);
const digestB64 = btoa(String.fromCharCode(...new Uint8Array(hashBuf)));
const resp = await fetch("https://nitro.occproof.com/commit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
digests: [{ digestB64, hashAlg: "sha256" }],
}),
});
const proofs = await resp.json();
// proofs[0] is a complete BitGraphProof/allocate-slotPre-allocate a causal slot before committing an artifact. The slot reserves a nonce and counter position, proving the enclave signed a sequence position before receiving any artifact content. Same key policy as /commit; metered per address in slots (a bare allocation holds one of the enclave's pending-slot entries for up to 120 seconds). The chain is bound at allocation and defaults to the anchored chain.
Request body (optional)
{ "chainId": "bitgraph:main" }Response (200)
{
"slotId": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"slot": {
"version": "bitgraph/slot/1",
"nonceB64": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"counter": "277",
"epochId": "a1b2c3d4e5f6...",
"publicKeyB64": "...",
"chainId": "bitgraph:main",
"signatureB64": "..."
},
"chainId": "bitgraph:main"
}Note: POST /commit handles slot allocation internally. A slot record carries no clock. The slotId is the slot's nonce: a bearer ticket until it is consumed, so do not disclose it before commit. Consuming a held slot over HTTP (POST /commit with slotId) is available only where the service enables it; a slot that is never consumed expires after 120 seconds. 429 with Retry-After when the per-address allocation budget is spent.
/api/fuse/allocateAllocate an unused slot for a fused artifact, before the artifact exists. No body. The slot exists before any hash reaches the enclave; the producer writes a commitment to the signed slot record into the artifact, then commits the artifact's digest under the same slot with POST /api/fuse/commit. Served by the site at https://bitgraph.ing. The route sits behind the anchor-first gate and a rotation guard: until the current epoch has an anchor, and in the window before the daily restart, it answers 503 tee-restarting.
Request body
(none)
Response (200)
{
"slotId": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"slot": {
"version": "bitgraph/slot/1",
"nonceB64": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=",
"counter": "277",
"epochId": "a1b2c3d4e5f6...",
"publicKeyB64": "...",
"chainId": "bitgraph:main",
"signatureB64": "..."
},
"chainId": "bitgraph:main"
}The slotId is the slot's nonce, a bearer ticket until it is consumed: write only the derived commitment into the artifact, never the nonce, and do not log it. The commitment is SHA-256 over the domain string bitgraph-fuse/1, a zero byte, the SHA-256 of the canonical slot record and the nonce. A slot that is never consumed expires after 120 seconds. 429 with Retry-After when the per-address allocation budget is spent.
/api/fuse/commitCommit the fused artifact's digest under the slot from /api/fuse/allocate. Exactly one digest. The signed attribution is the fused marker: name bitgraph-fuse/1, title the placement id, message the origin digest. An anchor must precede the slot in its epoch, otherwise 409 no-anchor-before-slot; that failure is final for the slot, so allocate again. The route refuses to return a proof minted under any other slot (502 slot-mismatch). Served by the site at https://bitgraph.ing.
Request body
{
"slotId": "gTME79qH3fXQ5qXX0JxX6T5oGhFRLLw2BIUoeQai9Z8=", // the slot's nonce
"slot": { ... }, // the slot record from /api/fuse/allocate, verbatim
"digests": [{
"digestB64": "<SHA-256 of the fused bytes>",
"hashAlg": "sha256"
}],
"chainId": "bitgraph:main",
"attribution": {
"name": "bitgraph-fuse/1", // fixed value; marks a fused proof
"title": "trailer/1", // placement id: trailer/1 | container/1 | container/2 | produced/1
"message": "<origin digest, standard base64>"
}
}Response (200)
{
"proof": {
"version": "bitgraph/1",
"artifact": { "hashAlg": "sha256", "digestB64": "<SHA-256 of the fused bytes>" },
"commit": { "nonceB64": "...", "counter": "278", "slotCounter": "277", "slotHashB64": "...", "epochId": "..." },
"attribution": { "name": "bitgraph-fuse/1", "title": "trailer/1", "message": "..." },
"slotAllocation": { ... }, // the held slot
...
}
}An ordinary bitgraph/1 proof: slotAllocation is the held slot, commit.slotCounter its counter, commit.counter the commit position. If the response is lost, read the proof back by the fused digest and match commit.slotHashB64 against the hash of the slot record you hold. Errors: 400 body shape; 409 no-anchor-before-slot; 502 slot-mismatch; 503 tee-restarting or ledger-unavailable, retry.
/keyReturns the enclave's current Ed25519 public key, platform measurement, and enforcement tier. Useful for pinning allowedMeasurements and allowedPublicKeys in verification policy.
Response (200)
{
"publicKeyB64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
"measurement": "ac813febd1ac4261eff4a6c059f78a5ecfc8c577...",
"enforcement": "measured-tee"
}/verifyServer-side verification of a proof against an optional policy. Note: verification can also be done entirely client-side. No API call required.
Request body
{
"proof": { ... }, // complete BitGraphProof
"policy": { // optional VerificationPolicy
"requireEnforcement": "measured-tee",
"allowedMeasurements": ["ac813febd1ac4261..."],
"requireAttestation": true,
"minCounter": "100"
}
}Response (200)
// Success
{ "valid": true }
// Failure
{ "valid": false, "reason": "measurement not in allowed set" }/api/proofs/digest/{digest}Every causal position of a digest (url-safe base64, no padding). By the original's digest: its recordings and every fused artifact naming it as origin, by position, never ranked. By a fused artifact's digest: that proof with its origin. ?counter= (and ?epoch=) selects which position the lead proof describes; the default is the earliest recording. Served by the site at https://bitgraph.ing.
Response (200)
{
"proofs": [{ "proof": { ... } }], // the selected position; [] when the ledger holds nothing
"lookupKind": "recorded", // "recorded" | "origin-only" (only fused descendants exist)
"positions": [
{
"counter": "278",
"epoch": "<url-safe>",
"lowerTime": "2026-03-07T12:00:00.000Z", // anchor before, or null
"upperTime": "2026-03-07T12:00:12.000Z", // anchor after, or null
"kind": "recorded", // "recorded" | "fused"
"artifactDigest": "<url-safe>"
},
{
"counter": "301",
"epoch": "<url-safe>",
"lowerTime": "...",
"upperTime": "...",
"kind": "fused",
"artifactDigest": "<url-safe>", // the fused artifact's own digest
"placement": "trailer/1",
"fusedOrigin": "<url-safe>" // the origin digest named by the signed marker
}
],
"causalWindow": { "anchorBefore": { ... }, "anchorAfter": { ... } },
"anchorBlock": null
}An empty proofs list means the ledger holds nothing for this digest. 503 with "ledger unavailable" means the ledger could not be read; it is not an answer about the bytes.
/api/proofs/batchBatch form of the lookup: one round trip for up to 500 url-safe digests. Each entry lists that digest's positions with their kind. Served by the site at https://bitgraph.ing.
Request body
{ "digests": ["<digest-a>", "<digest-b>", "<digest-c>"] }Response (200)
{
"results": {
"<digest-a>": {
"proofs": [
{ "proof": { ... }, "writeTime": 1741496392841, "kind": "recorded" },
{ "proof": { ... }, "writeTime": 1741496410207, "kind": "fused" }
]
},
"<digest-b>": { "proofs": [] }, // nothing on the ledger
"<digest-c>": { "proofs": [], "unavailable": true } // the read failed; not an answer
}
}/healthHealth check. Returns 200 if the parent server is running and can communicate with the enclave.
Response (200)
{ "ok": true }Type definitions
BitGraphProof
interface BitGraphProof {
version: "bitgraph/1";
artifact: {
hashAlg: "sha256";
digestB64: string;
};
commit: {
nonceB64: string;
counter?: string; // decimal, monotonic
slotCounter?: string; // slot's counter (< commit counter)
slotHashB64?: string; // SHA-256 of canonical slot body
time?: number; // Unix ms
prevB64?: string; // chain link
epochId?: string; // hex SHA-256
};
signer: {
publicKeyB64: string; // Ed25519, 32 bytes
signatureB64: string; // Ed25519, 64 bytes
};
environment: {
enforcement: "stub" | "hw-key" | "measured-tee";
measurement: string;
attestation?: {
format: string; // e.g. "aws-nitro"
reportB64: string;
};
};
slotAllocation?: { // causal slot record
version: string; // "bitgraph/slot/1"
nonceB64: string;
counter: string;
time: number;
epochId: string;
publicKeyB64: string; // same enclave key
signatureB64: string; // Ed25519 over canonical slot body
};
agency?: unknown; // legacy; present on some older proofs
attribution?: { // signed; creator metadata, or the fused marker:
// name "bitgraph-fuse/1", title placement id, message origin digest
name?: string;
title?: string;
message?: string;
};
timestamps?: {
artifact?: TsaToken;
proof?: TsaToken;
};
metadata?: Record<string, unknown>;
claims?: Record<string, unknown>;
}VerificationPolicy
interface VerificationPolicy {
requireEnforcement?: "stub" | "hw-key" | "measured-tee";
allowedMeasurements?: string[];
allowedPublicKeys?: string[];
requireAttestation?: boolean;
requireAttestationFormat?: string[];
minCounter?: string;
maxCounter?: string;
minTime?: number;
maxTime?: number;
requireEpochId?: boolean;
requireActor?: boolean; // legacy
}TsaToken
interface TsaToken {
authority: string;
time: string; // ISO 8601
digestAlg: string;
digestB64: string;
tokenB64: string; // DER-encoded RFC 3161
}Error responses
| Status | Cause | Body |
|---|---|---|
| 400 | Invalid request body | { "error": "..." } |
| 401 | Missing or invalid API key | { "error": "unauthorized" } |
| 413 | Payload too large | { "error": "Image too large. Max 2 MB." } |
| 409 | No anchor precedes the slot in its epoch (fuse commit); allocate again | { "error": "...", "code": "no-anchor-before-slot" } |
| 502 | The boundary committed under a different slot (fuse commit) | { "error": "...", "code": "slot-mismatch" } |
| 503 | Boundary restarting or not yet anchored, or the ledger could not be read; retry | { "error": "...", "code": "tee-restarting" } |
| 500 | Enclave / internal error | { "error": "internal server error" } |