Arcaveli API

Zero-knowledge AI proxy. Every response is encrypted to a key only you hold — even if our database is fully compromised, attackers see ciphertext.

npm install arcaveli-client v1.0.0 Encrypted-only · RSA-OAEP + AES-256-GCM

1. Quick start

Three steps: generate an API key from your dashboard, run onboarding once to create your encryption keypair, then send your first chat.

// Step 1 — Onboard once. Run this from a script, save the privateKey
//          to a secrets manager. The server NEVER stores it.
const { ArcaveliClient } = require('arcaveli-client');
const { privateKey } = await ArcaveliClient.onboard({
  apiKey: 'arc_xxxxx'
});
// Save to .env: ARCAVELI_PRIVATE_KEY=<value>

// Step 2 — Use forever
const client = new ArcaveliClient({
  apiKey:     process.env.ARCAVELI_API_KEY,
  privateKey: process.env.ARCAVELI_PRIVATE_KEY
});

const response = await client.chat([
  { role: 'user', content: 'Review this contract' }
]);
console.log(response.content); // decrypted automatically

Install via npm — the package has zero runtime dependencies and ships with TypeScript declarations:

npm install arcaveli-client

Or, if you'd rather not touch npm, drop the single-file build straight into your project: /js/arcaveli-client.js. Same code as the npm package — usable in Node, the browser, an edge worker, or anywhere else fetch + crypto exist.

Python equivalent

No SDK needed — Python's standard requests + the cryptography package handle the wire protocol and the RSA-OAEP / AES-GCM envelope. pip install requests cryptography and you're set.

import json, base64, requests, os
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

API_KEY  = os.environ["ARCAVELI_API_KEY"]
BASE_URL = "https://arcaveli.com"
HEADERS  = {"Authorization": f"Bearer {API_KEY}"}

# Step 1 — Onboard once. Run this from a script, save the privateKey
#          to a secrets manager. The server NEVER stores it.
def onboard():
    r = requests.post(f"{BASE_URL}/api/v1/onboard", headers=HEADERS)
    r.raise_for_status()
    return r.json()["privateKey"]   # PEM string — store now, can't recover

# Step 2 — Decrypt the envelope the server returns
def decrypt(envelope_b64, private_key_pem):
    env = json.loads(base64.b64decode(envelope_b64))
    pk  = serialization.load_pem_private_key(private_key_pem.encode(), password=None)
    aes_key = pk.decrypt(
        base64.b64decode(env["encryptedKey"]),
        padding.OAEP(mgf=padding.MGF1(hashes.SHA256()),
                     algorithm=hashes.SHA256(), label=None),
    )
    iv  = base64.b64decode(env["iv"])
    ct  = base64.b64decode(env["ciphertext"])
    tag = base64.b64decode(env["authTag"])
    return AESGCM(aes_key).decrypt(iv, ct + tag, None).decode()

# Step 3 — Use forever
def chat(messages, private_key_pem, model="claude-sonnet-4-6"):
    r = requests.post(f"{BASE_URL}/api/v1/chat/completions",
                      headers={**HEADERS, "Content-Type": "application/json"},
                      json={"model": model, "messages": messages})
    r.raise_for_status()
    body = r.json()
    ciphertext = body["choices"][0]["message"]["content"]["ciphertext"]
    return decrypt(ciphertext, private_key_pem), body["usage"]

# Example usage:
#   priv = onboard()   # save priv to a file once, then load it on every run
#   text, usage = chat([{"role": "user", "content": "Review this contract"}], priv)
#   print(text)

Same envelope shape applies to every encrypted field — file extraction text, conversation messages, canvas Delta — so this decrypt() function is the only crypto code you need across the whole API.

2. Authentication

Generating a key

Open your dashboard and click Generate API key. The key is shown once; copy it immediately. After you close the modal there is no way to recover it — only revoke + regenerate.

Sending the key

All API requests use a Bearer token in the Authorization header. Keys begin with arc_ followed by 64 hex chars.

Authorization: Bearer arc_4a402a2adc7a3724a486d4802d10aa3c80d4cb2f1c42b89c0dce1c6311bd3e66

Onboarding (one-time)

Before any other endpoint will respond, you must call POST /api/v1/onboard exactly once per API key. The server generates an RSA-2048 keypair, persists the public key, and returns the private key to you. This is the only time the server sees the private key — it is not logged, not stored, not recoverable.

POST /api/v1/onboard
Critical: save the returned privateKey to a secrets manager immediately. If lost, your encrypted history is unrecoverable — there is no escrow, no recovery, no support workaround. This is the entire point of the zero-knowledge model.

You can onboard on the same account you use for the web app. The API gets its own keypair (users.api_rsa_public_key) — separate from the browser-held key the web app uses (users.rsa_public_key) — so each surface keeps its own encrypted history. Re-onboarding the same key with onboard called twice returns 409; revoke the key in the dashboard and generate a new one to re-onboard.

3. Zero-knowledge encryption

How it works

Every chat response, file upload extraction, and stored document is encrypted with a fresh AES-256-GCM key. That symmetric key is then wrapped with your RSA-2048 public key (RSA-OAEP-SHA256). The opaque envelope is what gets persisted. Only your matching private key — held client-side in your secrets manager — can unwrap it.

What Arcaveli stores

What Arcaveli cannot access

What happens if your private key is lost

Your encrypted history becomes permanently unreadable. You'll need to revoke the API key from the dashboard, generate a new one, and re-onboard. Past ciphertext stays in our database as inert noise (or you can DELETE the conversations from the API).

4. Endpoints

Base URL: https://arcaveli.com/api/v1

Chat completions

POST /api/v1/chat/completions

OpenAI-compatible request shape. messages is an array of { role, content }; the response carries an encrypted ciphertext envelope.

// Request
{
  "model": "claude-sonnet-4-6",
  "messages": [
    { "role": "user", "content": "Summarize this lease" }
  ],
  "stream": false,
  "max_tokens": 1000,
  "agent": "case-research",           // optional
  "conversation_id": "uuid…"           // optional
}

// Response
{
  "id": "arc_msg_…",
  "object": "chat.completion",
  "created": 1714499000,
  "model": "claude-sonnet-4-6",
  "conversation_id": "…",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": {
        "encrypted": true,
        "ciphertext": "base64-envelope…",
        "encoding": "rsa-oaep-aes-256-gcm"
      }
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 120,
    "completion_tokens": 450,
    "total_tokens": 570,
    "estimated_cost_usd": 0.0072
  }
}

Pass stream: true to receive SSE chunks. Each chunk is a chat.completion.chunk with the encrypted envelope inside; the stream ends with data: [DONE]. The client library decrypts each chunk for you and emits it via onChunk(text).

Models

GET /api/v1/models

Returns models available to your account (filtered by plan and BYOK keys on file). OpenAI-compatible { object: 'list', data: [...] } shape.

Agents

GET /api/v1/agents

Lists built-in research agents. Pass an agent's id as the agent field on a chat call to enable its tool loop (CourtListener, PubMed, etc.).

Conversations

GET /api/v1/conversations
GET /api/v1/conversations/:id
POST /api/v1/conversations
DELETE /api/v1/conversations/:id

List, fetch, create, or delete encrypted conversation history. Message bodies travel as ciphertext envelopes; the client library decrypts them for you.

Files

POST /api/v1/files
GET /api/v1/files
DELETE /api/v1/files/:id

Upload a PDF, DOCX, or TXT (≤ 10 MB). The server extracts the text, encrypts it to your public key, and returns the plaintext extraction once in the upload response so you can attach it to a subsequent chat call.

Canvas (encrypted documents)

POST /api/v1/canvas
GET /api/v1/canvas
GET /api/v1/canvas/:id
PUT /api/v1/canvas/:id
DELETE /api/v1/canvas/:id
POST /api/v1/canvas/:id/export

CRUD for rich-text documents stored as encrypted Quill Deltas. POST /:id/export takes the freshly-decrypted delta in the body (server cannot read the stored ciphertext) and returns a DOCX buffer.

5. OpenAI compatibility

The chat shape mirrors OpenAI's so you can point existing OpenAI client code at us — but the response body is encrypted, so the official openai SDK won't decrypt it for you. Use arcaveli-client for auto-decryption, or call _decrypt(ciphertext) yourself.

const openai = new OpenAI({
  apiKey:  'arc_xxxxx',
  baseURL: 'https://arcaveli.com/api/v1'
});
// Responses are encrypted — use arcaveli-client for auto-decrypt

6. Using agents

An agent wraps a system prompt and a set of tools the model can call (CourtListener for case law, PubMed for medical literature). Set the agent field and the model gets access to those tools for the duration of the request — your code doesn't need to handle the tool loop.

const response = await client.chat(messages, {
  agent: 'case-research'
});
// Agent automatically searched CourtListener and cited cases
console.log(response.toolsUsed);
// → [{ name: 'courtlistener_search', summary: '…', sources: [...] }, ...]

Available agents:

7. Recipes

Copy-paste patterns for the most common workflows. All examples use the Node client, but every call is a plain HTTP request — Python equivalents follow the same shape.

PDF Q&A

Upload a PDF, get its text back once, then ask Claude questions about it. The extracted text only travels inside this single request — at rest it's encrypted to your key.

const fs = require('fs');

// 1. Upload — server extracts text, returns it ONCE in the response
const file = await client.uploadFile(
  fs.readFileSync('./contract.pdf'),
  'contract.pdf',
  'application/pdf',
);

// 2. Inject the extracted text as a system message + ask your question
const r = await client.chat([
  { role: 'system', content: `Document content:\n\n${file.extracted_text}` },
  { role: 'user',   content: 'List every payment obligation, with section numbers.' },
]);

console.log(r.content);

Multi-turn conversation

Pass the conversation_id returned from the first call to give Claude memory across turns. The server persists each turn (encrypted) so a long-running session can resume after a process restart.

const messages = [{ role: 'user', content: 'My client is Acme Corp. Remember that.' }];

let r = await client.chat(messages);
const conversationId = r.conversationId;

messages.push({ role: 'assistant', content: r.content });
messages.push({ role: 'user',      content: 'Draft an NDA for them.' });

r = await client.chat(messages, { conversation_id: conversationId });
// r.content references "Acme Corp" without you having to repeat it.

Agent research with sources

The case-research and medical-literature agents run a server-side tool loop — search CourtListener / PubMed, fetch full text, synthesize an answer with citations — and return both the prose and a structured list of sources you can render as clickable cards.

const r = await client.chat(
  [{ role: 'user', content: '2019 9th Cir cases on FTC consent decrees, brief summary each.' }],
  { agent: 'case-research' },
);

console.log(r.content);

for (const tool of r.toolsUsed || []) {
  for (const src of tool.sources || []) {
    console.log(`- ${src.label} (${src.meta})\n  ${src.url}`);
  }
}

Streaming with progress events

Streaming returns a single encrypted envelope at the end (RSA-OAEP wraps an AES key per response — there's no incremental ciphertext). The value is the progress stream during agent loops: each tool call emits an event so you can show Searching CourtListener… instead of leaving the user staring at a spinner.

await client.chatStream(
  [{ role: 'user', content: 'Find recent appellate rulings on…' }],
  { agent: 'case-research' },
  (e) => {
    if (e.progress?.phase === 'tool_use') {
      console.log('Calling tools:', e.progress.tools.join(', '));
    }
    if (e.text) console.log('Final answer:', e.text);
  },
);

8. Errors

Every error response is JSON of shape { error: string, code?: string, ... }. The HTTP status code is always set; the optional code field disambiguates errors that share a status. Common codes:

Status code Meaning + fix
401Invalid or missing API key. Check the Authorization: Bearer arc_… header. If the key was just revoked, generate a new one in the dashboard.
402Active subscription required. Choose a plan at /pricing.
402byok_key_missingSolo plan picked a model from a provider you haven't added a key for. Add the key under Settings → BYOK or pick a different model.
402workspace_inactiveYour workspace owner cancelled. Owner can resubscribe to reactivate.
403Onboarding required. Call POST /api/v1/onboard first; the response includes the only copy of your private key.
403managed_anthropic_onlyStarter / Business plans only support Anthropic models. Either pick a claude-* model or upgrade to Solo (BYOK).
404Resource (conversation, file, document) not found OR not visible to your API surface. Note: web-app resources are NOT visible via /api/v1/* by design.
409Already onboarded with this API key. To re-onboard: revoke the key in the dashboard and generate a new one.
413Payload too large. File uploads cap at 10 MB; chat bodies cap at 4 MB.
429Rate limit hit (60 req/min/user). Response includes retry_after seconds and a Retry-After header.
429credits_exhaustedMonthly AI credits used up + no top-up balance. Buy a credit pack from the dashboard or wait until next billing cycle. Response includes credits_used + credits_limit.
429workspace_credits_exhaustedWorkspace pool exhausted. Owner can top up or wait for next cycle.
502Upstream provider (Anthropic / OpenAI / etc.) returned an error or timed out. Safe to retry with exponential backoff.

Every successful response includes X-Rate-Limit-Limit and X-Rate-Limit-Remaining headers so well-behaved clients can pace themselves before hitting 429.

9. Rate limits & billing

Rate limits

Billing

API usage is metered identically to web app usage. Returns 429 with code: credits_exhausted when both your monthly allotment and any topped-up credits are zero.

10. Changelog

Breaking and notable changes to the public API + the arcaveli-client package. Patch releases (bug fixes, doc tweaks) aren't listed.

1.0.0 — 2026-05-01

0.9.0 — 2026-04-30

Open dashboard · Security overview · Pricing · Support