Skip to main content

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:
ScopePermissions
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"
}
FieldTypeRequiredDescription
typestringYesEvent type: agent_execution | workflow_step | human_approval | ai_recommendation | automated_action | policy_decision | escalation | custom
actorobjectYesWho/what made the decision
actor.idstringYesActor identifier
actor.typestringYeshuman | ai_agent | system | service
actor.namestringNoDisplay name
actionobjectYesWhat action was taken
action.typestringYesAction type identifier
action.descriptionstringNoHuman-readable description
action.inputobjectNoAction input data (any JSON)
action.outputobjectNoAction output data (any JSON)
aiContextobjectNoAI involvement details
aiContext.modelstringNoModel name (e.g., gpt-4, claude-sonnet-4-5-20250514)
aiContext.providerstringNoAI provider (e.g., openai, anthropic)
aiContext.confidencenumberNoConfidence score (0–1)
aiContext.reasoningstringNoDecision reasoning
tagsstring[]NoFilterable tags for categorization
idempotencyKeystringNoPrevents 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:
ParameterTypeDescription
typestringFilter by event type (e.g., agent_execution)
statusstringrecorded | sealed | approved | rejected
tagstringFilter by a single tag
limitnumberMax results, 1–100 (default: 20)
offsetnumberPagination 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"
}
FieldTypeRequiredDescription
approverobjectYesWho approved the decision
approver.idstringYesApprover identifier
approver.typestringYeshuman | system
approver.namestringNoDisplay name
resultstringYesapproved | rejected
reasonstringNoReason 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"
  }
}
FieldDescription
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)
evidenceLevelDRAFTDOCUMENTEDAUDIT_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

StatusCodeDescriptionCommon Cause
400BAD_REQUESTMalformed requestInvalid JSON body
401UNAUTHORIZEDInvalid or missing API keyWrong key, expired key, missing header
403FORBIDDENInsufficient scopeproof:read key trying to write
404NOT_FOUNDEvent not foundInvalid event ID, evidence not yet sealed
409CONFLICTEvent already sealedAttempting to approve a sealed event
422VALIDATION_ERRORInvalid request bodyMissing required fields, invalid types
429RATE_LIMITToo many requestsRetry after a short delay
500INTERNAL_ERRORInternal errorContact 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.