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:read | Query events, get evidence, export reports |
proof:write | Record 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 |
|---|
type | string | Yes | Event type: agent_execution | workflow_step | human_approval | ai_recommendation | automated_action | policy_decision | escalation | custom |
actor | object | Yes | Who/what made the decision |
actor.id | string | Yes | Actor identifier |
actor.type | string | Yes | human | ai_agent | system | service |
actor.name | string | No | Display name |
action | object | Yes | What action was taken |
action.type | string | Yes | Action type identifier |
action.description | string | No | Human-readable description |
action.input | object | No | Action input data (any JSON) |
action.output | object | No | Action output data (any JSON) |
aiContext | object | No | AI involvement details |
aiContext.model | string | No | Model name (e.g., gpt-4, claude-sonnet-4-5-20250514) |
aiContext.provider | string | No | AI provider (e.g., openai, anthropic) |
aiContext.confidence | number | No | Confidence score (0–1) |
aiContext.reasoning | string | No | Decision reasoning |
tags | string[] | No | Filterable tags for categorization |
idempotencyKey | string | 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 |
|---|
type | string | Filter by event type (e.g., agent_execution) |
status | string | recorded | sealed | approved | rejected |
tag | string | Filter by a single tag |
limit | number | Max results, 1–100 (default: 20) |
offset | number | 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 |
|---|
approver | object | Yes | Who approved the decision |
approver.id | string | Yes | Approver identifier |
approver.type | string | Yes | human | system |
approver.name | string | No | Display name |
result | string | Yes | approved | rejected |
reason | string | 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.hash | SHA-256 hash of this event (content + previousHash + timestamp) |
chain.previousHash | Hash of the preceding event in the chain (null for genesis) |
chain.index | Position in the sequential chain |
chain.domain | Hash chain domain (e.g., proof) |
evidenceLevel | DRAFT → 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_REQUEST | Malformed request | Invalid JSON body |
| 401 | UNAUTHORIZED | Invalid or missing API key | Wrong key, expired key, missing header |
| 403 | FORBIDDEN | Insufficient scope | proof:read key trying to write |
| 404 | NOT_FOUND | Event not found | Invalid event ID, evidence not yet sealed |
| 409 | CONFLICT | Event already sealed | Attempting to approve a sealed event |
| 422 | VALIDATION_ERROR | Invalid request body | Missing required fields, invalid types |
| 429 | RATE_LIMIT | Too many requests | Retry after a short delay |
| 500 | INTERNAL_ERROR | Internal 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.