API Reference

Official SDKs are cyphrex on npm and cyphrex on PyPI. There is no public source repository. getcyphrex/cyphrex-sdk, getcyphrex/node-sdk, and getcyphrex/python-sdk do not exist. Do not invent a GitHub clone URL or a scoped name such as @cyphrex/sdk. Machine-readable spec: /openapi.json and /llms.txt. Integration endpoint: POST /v1/check.

Base URL

Production API:

https://cyphrexapi-production.up.railway.app

Same routes are also proxied from the site as https://cyphrex.io/api/v1/.... There is no api.cyphrex.io.

Authentication

Three schemes share the Authorization header name. Use the scheme that matches the route. Mixing them returns 401.

1. API key — enforcement and MCP HTTP routes. Primary header first:

x-cyphrex-key: cprx_live_xxxxxxxxxxxx

Also accepted:

Authorization: Bearer cprx_live_xxxxxxxxxxxx

The official SDKs send both. x-cyphrex-key is checked first. This is not a dashboard session token.

2. Session JWT — dashboard routes. After login, Authorization: Bearer <Supabase session JWT>. An API key in this header on a dashboard route is not a valid session and will 401.

3. MCP session — remote MCP at https://mcp.cyphrex.io/mcp uses Authorization: Bearer cprx_live_... (the API key) on the MCP connection, not a JWT.

API key routes

These accept x-cyphrex-key or Authorization: Bearer cprx_live_....

POST /v1/check — Check an action before execution. This is the integration endpoint.

Required JSON body: agentId (UUID) and nested action.type. A top-level actionType is rejected with 400.

POST /v1/check
Content-Type: application/json
x-cyphrex-key: cprx_live_xxxxxxxxxxxx

{
  "agentId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "action": {
    "type": "http_call",
    "url": "https://api.example.com/data",
    "payload": { "method": "GET" }
  }
}

Optional on action: url, payload, metadata, dataScope (for data_access), estimatedCost, tool_name / toolName.

Success (200):

{ "allowed": true }

Policy deny (still 200):

{ "allowed": false, "reason": "URL not in allowed list", "severity": "P2" }

Some deny paths also include violation_type (for example rate_limit_exceeded, off_limits_action, unsafe_output). The API does not return a violations object array or a spend object on this route.

Other statuses: 400 missing/invalid body; 401 bad or missing key; 402 sandbox expired or cap; 403 frozen agent; 404 unknown or inactive agent; 429 paid action cap; 503 auth, write, or deadline failure (fail-closed: allowed: false).

POST /v1/mcp/... — MCP HTTP helpers (register, check, log, scan). Same API key auth as /v1/check. Prefer the hosted MCP server at https://mcp.cyphrex.io/mcp for tool access.

Public routes (no authentication)

  • GET /v1/keys/:keyId — signing public key PEM (text/plain)
  • GET /v1/keys — published key registry JSON
  • GET /v1/verify/:ssn — public blockchain SSN lookup
  • POST /v1/inclusion/verify — Merkle inclusion check
  • GET /v1/reports/:id/public — opted-in signed report JSON
  • GET /v1/reports/by-pdf-sha256/:sha256 — report JSON bound to a PDF hash
  • POST /v1/solana/verify — confirm a Solana tx references a report hash

Dashboard routes (session JWT)

These require Authorization: Bearer <session JWT> from POST /v1/auth/login. They do not accept the API key. There is no GET /v1/audit and no POST /v1/behavior-profiles.

POST /v1/agents — Create an agent. Body: name (required), optional type (or agent_type), purpose, version. Returns the agent row. A behavior profile is created empty; there is no behaviorProfileId field.

GET /v1/events — Audit log. Query: agentId, from, to, limit, offset. Returns { events, total, limit, offset }.

PATCH /v1/agents/:id/profile — Update the agent behavior profile. Body may include allowed_apis, blocked_apis, spending_limit, freeze_on_violation.

POST /v1/agents/:id/apply-package — Copy a safety package's rules onto that agent's behavior profile.

GET /v1/alerts — Violation inbox (blocked events)

Returns violations since last acknowledge. Configure destinations via GET/POST /v1/alert-configs. GET /v1/alerts/open-count drives the sidebar badge. POST /v1/alerts/acknowledge clears open count.

GET /v1/alert-configs — Notification destinations (email, webhook)

POST /v1/events/export-signed — Generate signed compliance report

Requires: Core, Scale or Enterprise plan (admin accounts bypass plan gate).

Request body: agentIds (array of UUID strings, optional — all agents if empty), format ("json" | "pdf"), frameworks (non-empty string[]: "soc2", "euaiact", "hipaa", "sr11-7", "sr26-2", "nydfs"), from (ISO timestamp), to (ISO timestamp). The singular framework field is no longer accepted.

Returns a signed compliance evidence package (schemaVersion 1.0.0) containing:

  • sha256 hash of RFC 8785 JCS canonical report bytes (canonicalization: "rfc8785-jcs")
  • Ed25519 signature (verify against GET /v1/keys/:keyId)
  • blockchain SSN and anchor history (if agent is registered)
  • inclusionProofs: per-event proof of inclusion in the on-chain root when that event was in an hourly tree. Unanchored events (current hour, or hours with no stored path) are { eventId, anchored: false } only. Membership of a closed leaf, not completeness.
  • per-event controls from stored mappings filtered to requested frameworks
  • status per action: Satisfied | Prevented | Gap, review required
  • PDF binary or JSON object depending on format

Verifying a signed JSON report (v1.0.0)

The signed payload is tagged canonicalization: "rfc8785-jcs". That is RFC 8785 JSON Canonicalization Scheme: lexicographic object-key order, the RFC's number format (so 1e-7 not 1e-07), and unescaped Unicode in strings. Sorted-key JSON.stringify / Python json.dumps are not JCS and will not verify.

The timestamp object (including issued_at, anchor, anchor_tx_signature, anchor_block_height, and message) is part of the signed payload. Strip only the top-level verification fields (sha256, signature, algorithm, publicKeyUrl) before canonicalizing. Keep canonicalization inside the signed core.

Use a JCS library in the language you verify from. Node: npm canonicalize (erdtman). Python: PyPI rfc8785. Verify the Ed25519 signature over the UTF-8 JCS bytes first, then SHA-256 those same bytes and compare to report.sha256. Do not stop at the hash: an attacker who tampers a field can recompute sha256.

Node.js

// schemaVersion 1.0.0 — RFC 8785 JSON Canonicalization Scheme (JCS).
// report.canonicalization === "rfc8785-jcs"
// npm install canonicalize@2

const canonicalize = require('canonicalize');
const { createHash, verify, createPublicKey } = require('crypto');

const STRIP = new Set(["sha256", "signature", "algorithm", "publicKeyUrl"]);
const core = Object.fromEntries(Object.entries(report).filter(([k]) => !STRIP.has(k)));
if (core.canonicalization !== "rfc8785-jcs") {
  throw new Error(`Unsupported canonicalization: ${core.canonicalization}`);
}

const canonical = canonicalize(core);

const publicKey = createPublicKey(publicKeyPem);
const isValid = verify(
  null,
  Buffer.from(canonical, 'utf8'),
  publicKey,
  Buffer.from(report.signature, 'base64')
);
if (!isValid) throw new Error("Invalid signature");

const computedHash = createHash('sha256').update(canonical, 'utf8').digest('hex');
if (computedHash !== report.sha256) throw new Error('Hash mismatch');
console.log('Valid signature');

Python (JCS library + cryptography only — no Cyphrex code)

This snippet checks the Ed25519 signature. It does not walk the Merkle path.

# pip install rfc8785 cryptography
# report.canonicalization == "rfc8785-jcs"
# Fetch the PEM (this is text/plain). https://cyphrex.io/keys/<keyId> is HTML.
# curl -fsS -o cyphrex-public.pem https://cyphrex.io/api/keys/cyphrex-signer-prod-01
import base64, hashlib, json
import rfc8785
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.serialization import load_pem_public_key

STRIP = ("sha256", "signature", "algorithm", "publicKeyUrl")

report = json.loads(open("cyphrex-report.json", encoding="utf-8").read())
pem = open("cyphrex-public.pem", "rb").read()
if report.get("canonicalization") != "rfc8785-jcs":
    raise SystemExit("Unsupported canonicalization: " + str(report.get("canonicalization")))

core = {k: v for k, v in report.items() if k not in STRIP}
canonical = rfc8785.dumps(core)
if isinstance(canonical, str):
    canonical = canonical.encode("utf-8")

# Check order: Ed25519 first (always), then SHA-256 of the same JCS bytes.
# Recomputing sha256 after a tamper does not make a forged package verify.
key = load_pem_public_key(pem)
try:
    key.verify(base64.b64decode(report["signature"]), canonical)
except InvalidSignature:
    raise SystemExit("Invalid signature")

digest = hashlib.sha256(canonical).hexdigest()
if digest != report["sha256"]:
    raise SystemExit("Hash mismatch")
print("Valid signature")

Independent inclusion check

The public verifier on /verify runs inclusion and the on-chain root in the browser. This Python verifier is the independent copy of those two checks. No account. It picks the first anchored event and prints the index.

Download verify_inclusion_independent.py. Allowed imports: rfc8785, cryptography, hashlib, stdlib. Raw PEM: /api/keys/cyphrex-signer-prod-01. Public packages: GET /v1/reports/:id/public for the sample IDs on /verify.

Setup

pip install rfc8785 cryptography
curl -fsS -o verify_inclusion_independent.py https://cyphrex.io/tools/verify_inclusion_independent.py
curl -fsS -o cyphrex-public.pem https://cyphrex.io/api/keys/cyphrex-signer-prod-01
curl -fsS -o report.json https://cyphrexapi-production.up.railway.app/v1/reports/8d539bd4-cc20-434f-9b10-093392dd3981/public
curl -fsS -o other.json https://cyphrexapi-production.up.railway.app/v1/reports/2c696df7-83e9-41d1-b08c-3692bad5f005/public

The first run should pass. The next three should each fail. That is the demonstration.

Run

python3 verify_inclusion_independent.py report.json cyphrex-public.pem
python3 verify_inclusion_independent.py report.json cyphrex-public.pem --tamper-event-url https://evil.example
python3 verify_inclusion_independent.py report.json cyphrex-public.pem --foreign-proof other.json
python3 verify_inclusion_independent.py report.json cyphrex-public.pem --reorder-sibling

Deploying the rfc8785-jcs cutover

  1. Deploy the API so new packages are sealed with RFC 8785.
  2. Re-seal existing rows: apps/api/scripts/regen-rfc8785-reports.js --apply against that database (refuses hosted URLs unless explicitly allowed).
  3. Deploy web so /verify requires canonicalization: "rfc8785-jcs".

Reverse that order and the public sample report IDs fail verification until step 2. Do not deploy web first.

POST /v1/inclusion/verify — Public inclusion check

No authentication required. Body is a whole package ({ package } or { report }) or a single { event, proof }. Recomputes the closed-leaf hash, walks the path, and searches the named Solana transaction for the claimed root. That is membership in the anchored tree, not completeness.

GET /v1/keys/:keyId — Public key registry

No authentication required. Returns the Ed25519 public key PEM for a given signing key ID as text/plain.

Example: GET /v1/keys/cyphrex-signer-prod-01

Fetch the PEM from cyphrex.io/api/keys/cyphrex-signer-prod-01 or GET /v1/keys/cyphrex-signer-prod-01 on the API. The HTML registry page at cyphrex.io/keys/cyphrex-signer-prod-01 is documentation, not the key.

GET /v1/agents/:id/anchors — Merkle anchor history. Session JWT, not the API key.

Returns on-chain Merkle anchors for an agent you own:

  • merkle_root
  • event_ids included in the batch
  • solana_tx_signature
  • solana_block_height
  • anchored_at timestamp

Anchors are written hourly for Core agents, every five minutes for Scale, and in real time for Enterprise high stakes actions as contracted.

Blockchain SSN API

Registration and transaction-payload routes use dashboard authentication: Authorization: Bearer <session JWT> (same token as the web app after login), not the API key. GET /v1/verify/:ssn is public and takes no credentials.

POST /v1/agents/:id/register-blockchain-self-custody

Register with self-custody. Available on all plans.

GET /v1/agents/:id/registration-tx returns a transaction payload for the user wallet to sign and submit.

Request body (JSON):

{
  "walletAddress": "<solana-pubkey>",
  "transactionSignature": "<signed-tx-from-user-wallet>"
}

API verifies the Solana transaction signature and stores blockchain_ssn, wallet_address, and solana_tx_signature.

GET /v1/verify/:ssn — Public verification

No authentication. Verifies an agent by its blockchain SSN (on-chain + Cyphrex metadata).

Response (200, example fields):

{
  "verified": true,
  "agent_type": "support",
  "registered_at": "<iso-timestamp>",
  "created_at": "<iso-timestamp>",
  "action_count": 42,
  "status": "Active",
  "owner_wallet": "<pubkey-or-null>"
}

status reflects on-chain state when available (e.g. Active). 404 if the SSN is unknown.

Message the founder