Proof API
Base URL: https://cronozen.com/api/v1
Authentication
The Proof API uses API Key authentication, not JWT tokens. Include your key in the Authorization header:
Authorization: Bearer czk_live_abc123...
API Key Scopes
Each API key has one or more scopes that control access:
Scope Permissions proof:readQuery events, get evidence, export reports proof:writeRecord events, add approvals
A key with proof:read cannot record or approve decisions. A key with proof:write can do everything.
# Read-only key — can query and export, but cannot record
curl https://cronozen.com/api/v1/decision-events \
-H "Authorization: Bearer czk_live_readonly_..."
# Write key — full access
curl -X POST https://cronozen.com/api/v1/decision-events \
-H "Authorization: Bearer czk_live_full_..." \
-H "Content-Type: application/json" \
-d '{ ... }'
API keys are hashed with SHA-256 before storage. The plaintext key is only shown once at creation time. Treat it like a password.
Proof API keys are separate from JWT tokens used for the rest of the Cronozen platform. A JWT token will not authenticate against the Proof API, and vice versa.
Endpoints
POST /v1/decision-events
Record a new decision event.
Scope required : proof:write
Request:
{
"type" : "agent_execution" ,
"actor" : {
"id" : "support_agent" ,
"type" : "ai_agent" ,
"name" : "Support Agent"
},
"action" : {
"type" : "refund_approved" ,
"description" : "Auto-approved refund based on policy threshold" ,
"input" : {
"orderId" : "ORD-1234" ,
"amount" : 45000
},
"output" : {
"refundId" : "REF-5678"
}
},
"aiContext" : {
"model" : "gpt-4" ,
"provider" : "openai" ,
"confidence" : 0.87 ,
"reasoning" : "Amount within auto-approval threshold"
},
"tags" : [ "refund" , "support" , "auto-approved" ],
"idempotencyKey" : "refund-ORD-1234-20260312"
}
Field Type Required Description typestring Yes Event type: agent_execution | workflow_step | human_approval | ai_recommendation | automated_action | policy_decision | escalation | custom actorobject Yes Who/what made the decision actor.idstring Yes Actor identifier actor.typestring Yes human | ai_agent | system | serviceactor.namestring No Display name actionobject Yes What action was taken action.typestring Yes Action type identifier action.descriptionstring No Human-readable description action.inputobject No Action input data (any JSON) action.outputobject No Action output data (any JSON) aiContextobject No AI involvement details aiContext.modelstring No Model name (e.g., gpt-4, claude-sonnet-4-5-20250514) aiContext.providerstring No AI provider (e.g., openai, anthropic) aiContext.confidencenumber No Confidence score (0–1) aiContext.reasoningstring No Decision reasoning tagsstring[] No Filterable tags for categorization idempotencyKeystring No Prevents duplicate recording (see below)
Response (201):
{
"data" : {
"id" : "cmmnhiobs0002bfi9mlu8eof4" ,
"decisionId" : "refund-ORD-1234-20260312" ,
"type" : "agent_execution" ,
"status" : "recorded" ,
"actor" : {
"id" : "support_agent" ,
"type" : "ai_agent" ,
"name" : "Support Agent"
},
"action" : {
"type" : "refund_approved" ,
"input" : { "orderId" : "ORD-1234" , "amount" : 45000 }
},
"occurredAt" : "2026-03-12T13:00:00.000Z" ,
"tags" : [ "refund" , "support" , "auto-approved" ],
"evidence" : {
"id" : "evi_x1y2z3" ,
"status" : "pending" ,
"chainHash" : null ,
"chainIndex" : null
},
"createdAt" : "2026-03-12T13:00:00.000Z" ,
"updatedAt" : "2026-03-12T13:00:00.000Z"
}
}
Idempotency
Pass an idempotencyKey to prevent duplicate events from retries or network issues. If the same key is sent again, the original event is returned (200) instead of creating a duplicate.
# First call — creates event
curl -X POST .../v1/decision-events \
-d '{ "idempotencyKey": "settle-STL-001", ... }'
# → 201 Created
# Retry with same key — returns original
curl -X POST .../v1/decision-events \
-d '{ "idempotencyKey": "settle-STL-001", ... }'
# → 200 OK (same event, no duplicate)
Use idempotency keys for any event that might be retried — settlement webhooks, queue consumers, or cron-triggered recordings.
GET /v1/decision-events
List decision events with filters.
Scope required : proof:read
Query parameters:
Parameter Type Description typestring Filter by event type (e.g., agent_execution) statusstring recorded | sealed | approved | rejectedtagstring Filter by a single tag limitnumber Max results, 1–100 (default: 20) offsetnumber Pagination offset (default: 0)
Example:
# Events tagged with "settlement", sealed only
curl "https://cronozen.com/api/v1/decision-events?tag=settlement&status=sealed&limit=50" \
-H "Authorization: Bearer czk_live_..."
Response (200):
{
"data" : [
{
"id" : "cmmnhiobs0002bfi9mlu8eof4" ,
"decisionId" : "settle-STL-001" ,
"type" : "automated_action" ,
"status" : "sealed" ,
"actor" : { "id" : "finance_bot" , "type" : "system" },
"action" : { "type" : "settlement_created" },
"tags" : [ "settlement" , "finance" ],
"createdAt" : "2026-03-12T13:00:00.000Z" ,
"updatedAt" : "2026-03-12T13:05:00.000Z"
}
],
"pagination" : {
"total" : 142 ,
"limit" : 50 ,
"offset" : 0 ,
"hasMore" : true
}
}
POST /v1/decision-events//approvals
Add human approval and seal the event with SHA-256.
Scope required : proof:write
Request:
{
"approver" : {
"id" : "team_lead" ,
"type" : "human" ,
"name" : "Kim"
},
"result" : "approved" ,
"reason" : "Verified against refund policy v2.1"
}
Field Type Required Description approverobject Yes Who approved the decision approver.idstring Yes Approver identifier approver.typestring Yes human | systemapprover.namestring No Display name resultstring Yes approved | rejectedreasonstring No Reason for the decision
Response (200):
{
"data" : {
"approvalId" : "apr_m1n2o3" ,
"decisionId" : "refund-ORD-1234-20260312" ,
"approver" : {
"id" : "team_lead" ,
"type" : "human" ,
"name" : "Kim"
},
"result" : "approved" ,
"reason" : "Verified against refund policy v2.1" ,
"evidenceLevel" : "AUDIT_READY" ,
"sealedHash" : "sha256:9943798c6313e9dd2cffa71686176d4125a65777..." ,
"sealedAt" : "2026-03-12T13:05:00.000Z" ,
"createdAt" : "2026-03-12T13:05:00.000Z"
}
}
Error — Already sealed (409):
{
"error" : {
"code" : "CONFLICT" ,
"message" : "Event is already sealed. Sealed events cannot be modified."
}
}
Once an event is sealed, it is immutable . Attempting to approve an already-sealed event returns 409 Conflict. This is by design — sealed events are part of the hash chain and cannot be altered without breaking chain integrity.
GET /v1/evidence/
Retrieve sealed evidence with full hash chain verification data.
Scope required : proof:read
Response (200):
{
"data" : {
"id" : "evi_x1y2z3" ,
"decisionId" : "refund-ORD-1234-20260312" ,
"status" : "sealed" ,
"evidenceLevel" : "AUDIT_READY" ,
"event" : {
"type" : "agent_execution" ,
"actor" : { "id" : "support_agent" , "type" : "ai_agent" },
"action" : {
"type" : "refund_approved" ,
"input" : { "orderId" : "ORD-1234" , "amount" : 45000 }
},
"occurredAt" : "2026-03-12T13:00:00.000Z" ,
"aiContext" : {
"model" : "gpt-4" ,
"confidence" : 0.87
}
},
"approval" : {
"approver" : { "id" : "team_lead" , "type" : "human" , "name" : "Kim" },
"result" : "approved" ,
"reason" : "Verified against refund policy v2.1" ,
"approvedAt" : "2026-03-12T13:05:00.000Z"
},
"chain" : {
"hash" : "sha256:9943798c6313e9dd2cffa71686176d4125a65777..." ,
"previousHash" : "sha256:7f3a8b2c..." ,
"index" : 42 ,
"domain" : "proof"
},
"sealedAt" : "2026-03-12T13:05:00.000Z" ,
"createdAt" : "2026-03-12T13:00:00.000Z"
}
}
Field Description chain.hashSHA-256 hash of this event (content + previousHash + timestamp) chain.previousHashHash of the preceding event in the chain (null for genesis) chain.indexPosition in the sequential chain chain.domainHash chain domain (e.g., proof) evidenceLevelDRAFT → DOCUMENTED → AUDIT_READY
The evidence.get() endpoint returns the full event payload including input data, AI context, and chain position. Use this for compliance checks and chain integrity verification. Use evidence.export() when you need a portable audit document.
GET /v1/evidence//export
Export a sealed event as a JSON-LD v2 audit document.
Scope required : proof:read
Response (200):
{
"@context" : "https://schema.cronozen.com/decision-proof/v2" ,
"@type" : "DecisionProof" ,
"version" : "2.0" ,
"exportedAt" : "2026-03-12T14:00:00.000Z" ,
"evidence" : {
"id" : "evi_x1y2z3" ,
"decisionId" : "refund-ORD-1234-20260312" ,
"status" : "sealed" ,
"evidenceLevel" : "AUDIT_READY" ,
"event" : { "..." : "..." },
"approval" : { "..." : "..." },
"chain" : { "..." : "..." }
},
"verification" : {
"hashAlgorithm" : "SHA-256" ,
"chainDomain" : "proof" ,
"chainIndex" : 42 ,
"chainHash" : "sha256:9943798c..." ,
"previousHash" : "sha256:7f3a8b2c..." ,
"verifyUrl" : "https://cronozen.com/verify/cmmnhiobs0002bfi9mlu8eof4"
}
}
This document is self-contained and can be stored, shared, or submitted to auditors independently of the Cronozen platform.
Error Reference
Status Code Description Common Cause 400 BAD_REQUESTMalformed request Invalid JSON body 401 UNAUTHORIZEDInvalid or missing API key Wrong key, expired key, missing header 403 FORBIDDENInsufficient scope proof:read key trying to write404 NOT_FOUNDEvent not found Invalid event ID, evidence not yet sealed 409 CONFLICTEvent already sealed Attempting to approve a sealed event 422 VALIDATION_ERRORInvalid request body Missing required fields, invalid types 429 RATE_LIMITToo many requests Retry after a short delay 500 INTERNAL_ERRORInternal error Contact support
All error responses follow this format:
{
"error" : {
"code" : "CONFLICT" ,
"message" : "Event is already sealed. Sealed events cannot be modified." ,
"details" : {}
}
}
409 Conflict — Sealed Event
The most common non-trivial error. Occurs when calling POST /v1/decision-events/{id}/approvals on an event that has already been approved and sealed.
Why this happens:
Concurrent approval attempts (two approvers clicking simultaneously)
Retry logic re-sending an already-successful approval
Webhook handler firing multiple times
How to handle:
import { ConflictError } from "cronozen"
try {
await cz . decision . approve ( eventId , payload )
} catch ( e ) {
if ( e instanceof ConflictError ) {
// Already sealed — this is fine, the approval succeeded previously
const existing = await cz . evidence . get ( eventId )
console . log ( "Already sealed at:" , existing . sealedAt )
}
}
Use idempotencyKey on the original decision.record() call to prevent duplicate events upstream.
Proof SDK TypeScript SDK reference with error handling and integration patterns
Proof Pipeline How the 4-stage evidence pipeline works under the hood