# API Reference {#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](/openapi.json) and [/llms.txt](/llms.txt). Integration endpoint: `POST /v1/check`.

### Base URL

Production API:

```text
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 {#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:

```text
x-cyphrex-key: ${CYPHREX_API_KEY}
```

Also accepted:

```text
Authorization: Bearer ${CYPHREX_API_KEY}
```

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 ${CYPHREX_API_KEY}` (your API key) on the MCP connection, not a JWT. Tools: `register_agent`, `check_action`, and `get_audit_log`. Setup: [Use Cyphrex with your AI](/docs/ai).

### API key routes

These accept `x-cyphrex-key` or `Authorization: Bearer ${CYPHREX_API_KEY}`.

**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.

```text
POST /v1/check
Content-Type: application/json
x-cyphrex-key: ${CYPHREX_API_KEY}

{
  "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):

```json
{ "allowed": true }
```

Expired free trial (still 200). Enforcement is paused, the action is recorded, and nothing is blocked:

```json
{ "allowed": true, "enforcement": "off", "reason": "trial_expired", "code": "trial_expired" }
```

The trial recorded-action cap is 100,000. Actions after that cap are still stored, including after day 14. The count shown to a trial account, in the dashboard and on `GET /v1/billing/usage`, stays at 100,000 until the account is paid. A paid account is shown the true count. The check does not return `recorded: false`.

Policy deny (still 200):

```json
{ "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 or invalid body; 401 bad or missing key; 402 enforced-action cap while the trial is active (25,000 enforced actions). The 100,000 recorded-action cap does not return 402 and does not stop storage. After day 14, checks are HTTP 200, blocking stops, and actions keep being recorded. 403 frozen agent; 404 unknown or inactive agent; 429 request rate limit (paid accounts are not blocked or throttled for volume; overage is billed); 503 auth, write, or deadline failure (fail-closed: `allowed: false`).

**POST /v1/mcp/...** — HTTP helpers behind the hosted MCP server. Same API key auth as `/v1/check`. Prefer `https://mcp.cyphrex.io/mcp`. The tools are `register_agent`, `check_action`, and `get_audit_log`.

### 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 a signed compliance evidence package.

**Requires:** a paid or design-partner account. A free trial ends with one day-14 risk report, sealed automatically.

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 the 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, or 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

```javascript
// 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.

```python
# 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 {#independent-inclusion-check}

The public verifier on [/verify](/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](/tools/verify_inclusion_independent.py). Allowed imports: rfc8785, cryptography, hashlib, stdlib. Raw PEM: [/api/keys/cyphrex-signer-prod-01](/api/keys/cyphrex-signer-prod-01). Public packages: `GET /v1/reports/:id/public` for the sample IDs on [/verify](/verify).

#### Setup

```bash
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

```bash
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
```

**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](https://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](https://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

Anchoring on paid accounts: daily is included. Hourly is $150 a month. Every 5 minutes is $750 a month. Real-time is that $750 plus $25 per 1,000 on-chain authorizations.

### Blockchain SSN API {#blockchain-endpoints}

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):

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

The 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 and Cyphrex metadata).

**Response** (200, example fields):

```json
{
  "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 (for example Active). **404** if the SSN is unknown.
