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.appSame 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_xxxxxxxxxxxxAlso accepted:
Authorization: Bearer cprx_live_xxxxxxxxxxxxThe 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 JSONGET /v1/verify/:ssn— public blockchain SSN lookupPOST /v1/inclusion/verify— Merkle inclusion checkGET /v1/reports/:id/public— opted-in signed report JSONGET /v1/reports/by-pdf-sha256/:sha256— report JSON bound to a PDF hashPOST /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
controlsfrom 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/publicThe 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-siblingDeploying the rfc8785-jcs cutover
- Deploy the API so new packages are sealed with RFC 8785.
- Re-seal existing rows:
apps/api/scripts/regen-rfc8785-reports.js --applyagainst that database (refuses hosted URLs unless explicitly allowed). - Deploy web so
/verifyrequirescanonicalization: "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_rootevent_idsincluded in the batchsolana_tx_signaturesolana_block_heightanchored_attimestamp
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.