Developer & enterprise

PsiGuard API documentation.

Send a chat turn to PsiGuard and get back a guarded answer plus a single, plain protection signal telling you whether the monitor stepped in. One authenticated endpoint, JSON in and JSON out — wire it into your assistant in a few minutes.

Overview

What the API does.

PsiGuard is a structural monitor for language-model output. It watches how a response holds together as it's generated and steps in when a response starts to come apart — rather than judging whether any particular statement is true. (For a tour of the live dashboard and what the readings mean, see the dashboard guide.)

The API exposes that protection over one door. You send a user message; PsiGuard runs one guarded generation through the same path the dashboard uses, and returns the answer along with a coarse signal telling you whether it intervened. The internal readings that drive the decision stay on our side and never cross the wire — your integration stays simple, and there's nothing sensitive to handle.

Base URL. All requests go to your PsiGuard host over HTTPS — for example https://psiguard.net. Paths below are relative to that host.
Authentication

Authenticate with an API key.

Every API request is authenticated with a PsiGuard API key. Keys are issued and managed from your dashboard under API keys. A key looks like psg_live_… and carries the permissions of the account that created it.

Send the key on every request using either header — pick one:

Authorization header
Authorization: Bearer psg_live_your_key_here
or X-API-Key header
X-API-Key: psg_live_your_key_here

Managing keys

From the API keys page you can:

  • Create a key — the full key is shown once at creation. Copy it then; you won't be able to see it again.
  • List your keys — you'll see a short prefix like psg_live_8f3k2x… for each one, so you can tell them apart without exposing the full value.
  • Revoke a key — revoking takes effect immediately. Rotate keys by creating a new one and revoking the old.
Treat keys like passwords. Store them as a server-side secret (an environment variable or a secrets manager), never in client-side code or a public repo. Anyone holding a key can spend your account's usage.
Quickstart

Your first guarded turn.

Send a prompt to the guard endpoint with your key. This is a complete, working request — it's a Terminal command, so paste the whole block into your Terminal exactly as it is (the backslashes continue one command across several lines):

curl
curl https://psiguard.net/v1/guard \
  -H "Authorization: Bearer psg_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "What is your return policy?",
    "system_prompt": "You are a helpful support assistant for Acme.",
    "conversation_id": "cust-8472"
  }'

You'll get back the guarded answer and a protection signal:

200 · application/json
{
  "conversation_id": "cust-8472",
  "answer": "Our return policy allows returns within 30 days…",
  "protection": "safe"
}
Endpoint

Guard a turn.

Runs one guarded generation and returns the answer. This is the only endpoint most integrations need.

POST /v1/guard API key

Request body

Send a JSON object with these fields:

prompt string · required
The user's message for this turn.
system_prompt string · optional
Your assistant's personality, scope, or instructions. Leave it out to run the model with no steering (still guarded).
conversation_id string · optional
A caller-chosen id that groups turns into one thread, so monitoring carries context across a conversation. Reuse the same id for every turn in a conversation. If omitted, PsiGuard starts a new conversation and returns its id in the response — send that id back on the next turn to continue it.
history array · optional
Prior turns you want the model to see, as objects of the form {"role": "...", "content": "..."}. PsiGuard uses these for context and does not retain them as conversation state.

Response

A JSON object with three fields:

conversation_id string
An echo of the id you sent, or the new id PsiGuard assigned if you didn't send one.
answer string
The guarded reply to show your user. If PsiGuard withheld the response, this is a neutral refusal — indistinguishable from an ordinary model decline.
protection string
One of safe, caution, or intervened — see below.
Reading the result

The protection signal.

Every response carries one coarse protection value, read off the same decision the dashboard uses. It tells you how much PsiGuard had to do on this turn — without exposing anything about how it decided.

protection values
safeA clean run. The response held together; PsiGuard didn't need to act.
cautionPsiGuard steered the response back on course mid-generation. The answer is the corrected one you should show.
intervenedPsiGuard withheld the response. The answer is a neutral refusal — safe to show as-is.

A simple integration can ignore the signal entirely and just display answer. If you want to surface protection in your own UI — say, a small "reviewed" indicator — caution and intervened are the turns worth flagging.

Multi-turn

Conversations.

PsiGuard reads a response in the context of the turns around it, so a conversation is monitored as a trajectory rather than a series of unrelated one-offs. Two things tie a thread together:

  • Reuse conversation_id across every turn in the same chat. PsiGuard keeps the thread's monitoring context under that id, which is what lets it tell a passing wobble from a building pattern.
  • Pass history so the model has the prior turns to answer from. You own and store the transcript on your side; send the slice you want the model to see.
multi-turn request
{
  "conversation_id": "cust-8472",
  "prompt": "And what about exchanges?",
  "history": [
    { "role": "user",      "content": "What is your return policy?" },
    { "role": "assistant", "content": "Our return policy allows returns within 30 days…" }
  ]
}
Models

Choosing a model.

PsiGuard guards a model — it doesn't replace one. Which model runs behind your account is set once in the dashboard, and every /v1/guard call uses it. You have three options:

  • Bring your own provider key — run on your own account with OpenAI, Anthropic (Claude), Google Gemini, DeepSeek, or xAI Grok. Add your key in the dashboard; it's stored encrypted, and your usage bills to your account.
  • Run the model on your own machine — keep generation entirely on your hardware and let PsiGuard guard it remotely. See below.
  • No key yet? Take the guided first flight on the dashboard — a short, fixed sequence of prompts on a model PsiGuard provides, built to show the readings move and PsiGuard react. The flight is dashboard-only: /v1/guard always runs on your own key or local model, so connect one of those before calling the API.
Your key, your account — no fallback. Runs on your saved provider always use your key; PsiGuard never quietly switches a call onto its own key. If your key is rejected or out of credit, generation pauses with a clear message until you add funds or update the key in the dashboard.

Whatever model runs behind your account, PsiGuard adapts to it: over its first stretch of traffic it learns that model's normal behavior across all four signals and then monitors against that — so the same guarding fits any model, one you bring or one we provide, without per-model tuning. Learning is deliberately picky about its teachers: a model's "normal" is built only from ordinary turns PsiGuard never had to touch, one settled reading per turn — corrected turns and demo runs never shape the baseline. This happens automatically server-side; there's nothing to configure and no field to pass. (You can watch it settle on the dashboard, where a Learning badge turns to Calibrated once a model's normal is established. Until then, guarding runs on PsiGuard's standard lines — nothing is unprotected while a model learns.)

Intervention sensitivity

Once a model's normal is established, you can choose how far outside it a reading must be before PsiGuard steps in. The dial lives in the dashboard's model settings and has three positions: Relaxed, Balanced, and Strict. It applies to your account only, and it never changes what PsiGuard measures — only how quickly it acts on what it sees.

Balanced works for almost everyone. Moving the dial is a real trade-off: Strict reacts sooner but may intervene on responses that were actually fine (over-reacting); Relaxed intervenes less but may let mild instability slide longer (under-reacting). Change it only if PsiGuard is stepping in too often for your use case (relax) or not soon enough (tighten).

The learning itself is shared per model and unaffected by anyone's dial — two accounts on the same model read from the same learned normal but can judge departures from it with different strictness.

Run the model on your own machine

If your model can't leave your hardware — regulated data, an air-gapped policy, or a fine-tune you don't want to ship anywhere — PsiGuard can guard it where it sits. You run a small relay agent next to your model; PsiGuard sends prompts to it and monitors the answer as it's written. The model, the weights, and the prompts never leave your machine.

It works with any local server that speaks the OpenAI-compatible protocol, which covers the common ones:

  • Ollama — http://localhost:11434/v1
  • LM Studio — http://localhost:1234/v1 (enable the local server first)
  • vLLM — http://localhost:8000/v1

Download the relay from the Connect a model panel in your dashboard, then start it with a PsiGuard API key. It needs Python 3 and the requests package:

Terminal · on the machine running your model
pip install requests
python3 psiguard_relay.py --token psg_live_your_key_here

When it prints Registered with PsiGuard you're connected. Leave that terminal open — closing it disconnects your model. Add --auto to detect the local endpoint automatically, or --scan to list what it can find and exit.

The relay only dials out. It opens no inbound port and exposes nothing on your machine to the internet — it connects to PsiGuard and waits for work. It's a courier: it carries prompts in and answers back out, and contains none of PsiGuard's internals.
A disconnected relay returns 400. Your local model counts as available only while the relay is actually connected and sending keepalives. If it stops — terminal closed, machine asleep, network dropped — /v1/guard answers 400 with model provider unavailable: relay rather than silently rerouting your traffic to a different model. Treat that as a signal to check the relay, and keep it running under a process supervisor for production use.
Errors & limits

Errors and limits.

Errors come back as a JSON object with an error field and a matching HTTP status:

error shape
{ "error": "missing 'prompt'" }
400The request was malformed — the body wasn't a JSON object, prompt was missing, or no model is configured for the account.
401The API key was missing or not valid. Check the header and that the key hasn't been revoked.
403own_key_required — the account has no model connected. Add your own provider key or connect the relay in the dashboard, then retry.
404From /v1/activity/<run_id>: no turn with that id exists on your account.
429Either you sent requests too quickly, or a usage limit was reached. These are different situations with different fixes — see below.
502The underlying model failed to produce a result. Safe to retry.

The two kinds of 429

A 429 always carries an error field naming which limit you hit. Read it before deciding what to do — one of these wants you to retry, and the other doesn't:

rate_limited too fast
You exceeded the per-minute request rate. Back off briefly and retry — the allowance refills on its own. Wrap calls in an exponential backoff and this resolves itself.
monthly_cap_reached plan limit
Your account's monthly guarded-turn allowance is spent. The body includes plan, used, cap, and month. Retrying will not help until the month rolls over — or move to a plan with a higher cap.

Rate limits

/v1/guard accepts up to 60 requests per minute per account (and /v1/activity has its own 60 per minute), under a wider backstop of 300 per minute and 5,000 per hour across all endpoints. Normal application traffic sits well inside this; you'll typically only meet it during a bulk backfill or a retry storm. Exceeding it returns rate_limited, not a usage error, and nothing is metered against your plan.

Retry the rate limit, not the cap. rate_limited clears by waiting a moment. monthly_cap_reached does not — retrying it just burns requests. Branch on the error value rather than on the status code alone.

Usage

Each call to /v1/guard is metered as one guarded turn. Your plan and current usage are shown in the account menu of the dashboard. Generation always runs on your own provider key or local model, so model costs bill to your provider account — PsiGuard meters monitoring only. Reading your activity (/v1/activity) is never metered, and requests rejected with rate_limited aren't either.

Activity

Review activity.

Every turn you send through /v1/guard is recorded on your account, so you can look back at what was asked, what your user received, and — when PsiGuard stepped in — what it saw. Browse it on the API activity page, or pull the same records into your own tools with the endpoints below. Turns run on the dashboard aren't included. Records are kept for 90 days, then deleted automatically. Reading activity runs no model and isn't metered.

Outcomes match your logs. Each turn's outcome is the same decision the API reported as protection when you made the call: delivered = safe, steered = caution, withheld = intervened.

List turns

GET /v1/activity API key
view optional
all (default) or stepped_in — only turns PsiGuard steered or withheld.
days optional
7, 30 (default), or 90.
q optional
Only turns whose conversation_id contains this text.
key optional
Only turns made with this key, by its prefix (e.g. psg_live_8f3k2x…).
before optional
For the next page: pass the next_before value from the previous response. Pages hold up to 50 turns, newest first.
200 · application/json
{
  "days": 30,
  "summary": { "total": 1204, "steered": 9, "withheld": 2 },
  "keys": ["psg_live_8f3k2x…"],
  "items": [
    {
      "run_id": "…",
      "ts": 1790000000.0,
      "conversation_id": "cust-8472",
      "key_prefix": "psg_live_8f3k2x…",
      "prompt": "first 200 characters of the prompt",
      "outcome": "steered",
      "protection": "caution",
      "verdict": { "token": "drift", "label": "Drift", "description": "…" }
    }
  ],
  "next_before": 1789990000.0
}

The summary counts cover the whole period and filters, not just the current page. next_before is null on the last page. ts is a Unix timestamp in seconds.

One turn in full

GET /v1/activity/<run_id> API key
prompt, received string
What was asked, and exactly what your user received. On a withheld turn, received is the neutral decline the API returned; the withheld draft is not kept.
moment object · null
When PsiGuard stepped in: the verdict at that moment (label, description) and its 0–100 risk. null on delivered turns.
verdict object
The reading at the end of the answer, with its risk.
series object
The four signals — Coherence, Drift, Entropy, Memory Coupling — as readings across the answer, start to finish. A null reading means it wasn't measured at that point.
cut_short boolean
true when the model provider ended the answer early (a length or provider limit) — separate from anything PsiGuard did.

A key can read the activity of the account it belongs to — every key on the account sees the same records — and never another account's.

Utility

Verify a key.

A lightweight endpoint to confirm a key is valid and see which account it belongs to — handy for a setup check or health probe. It runs no model and isn't metered.

GET /v1/whoami API key
200 · application/json
{
  "account": "acct_…",
  "auth_via": "api_key"
}
Libraries

SDKs.

Official client libraries for Python and JavaScript / TypeScript wrap everything on this page — guarded turns, conversations, key verification, and typed errors — so you write a few lines instead of hand-rolling HTTP. Both speak only the public contract: a prompt goes out, an answer and a protection signal come back. Working in another language? Any HTTP client works too — see the end of this section.

Both libraries are currently at version 1.1.0. They track the API described on this page; installing the latest is always the right move.

Where each block goes: blocks labeled Terminal are commands — paste those into your Terminal. Blocks labeled Python or TypeScript are program code — they live inside a file that your application runs (pasting them into the Terminal won't work). Each language below starts with a Terminal-only smoke test so you can see a guarded turn work before writing any files.

Python

The official Python client is a single self-contained module whose only dependency is requests.

Terminal
pip install psiguard

Prefer not to add a dependency? The client is one self-contained file — psiguard.py — that you can drop straight into your project. It needs only requests at runtime.

Try it from your Terminal — no files needed. Paste these two lines (with your real key) and you'll see a guarded answer come back:

Terminal
export PSIGUARD_API_KEY="psg_live_your_key_here"
python3 -c "from psiguard import Client; r = Client().guard('What is your return policy?'); print(r.answer, '·', r.protection)"

On Windows PowerShell, set the key with $env:PSIGUARD_API_KEY="psg_live_your_key_here" instead of export.

Your first guarded turn — in your application. Everything from here on is Python code: save it in a file (say guard_test.py) and run that file with python3 guard_test.py.

Python
from psiguard import Client

client = Client(api_key="psg_live_your_key_here")

result = client.guard("What is your return policy?")
print(result.answer)        # the reply to show your user
print(result.protection)    # "safe" | "caution" | "intervened"

Set PSIGUARD_API_KEY in your environment and you can construct the client with no arguments: Client(). To point at a non-default host, pass base_url= or set PSIGUARD_BASE_URL.

Seeing the status while you're testing

Those two print lines are how a real integration uses the result: your app shows answer to the user and does something with protection. But when you are the one at the keyboard — running a smoke test, debugging an assistant — a status printed in the same plain white as everything else is easy to skim right past. format_result renders the turn for a terminal instead:

Python
from psiguard import Client, format_result

result = Client().guard("What is your return policy?")
print(format_result(result))
Output
● SAFE  clean run — PsiGuard didn't need to act
│ Our return policy allows returns within 30 days of delivery...

▲ CAUTION  steered back on course mid-answer
│ I'd check with support on that specific case...

■ INTERVENED  response withheld — the answer below is a refusal
│ I'm not able to help with that.

Green dot, amber triangle, red square — so it still reads when it's colorless or the reader is colorblind. badge(result.protection) gives you the chip alone if you'd rather build your own layout.

These are display helpers, not the integration path. The client never prints, and never puts color codes inside result.answer — that string is what your user sees, and it may end up in a log, a database, or a web page, where terminal color turns into visible junk. format_result also drops to plain text automatically when output isn't a real terminal (piped to a file, running in CI) and honors NO_COLOR. Requires psiguard 1.1.0 or later.

Give your assistant a personality.

Python
result = client.guard(
    "Do you ship to Canada?",
    system_prompt="You are a friendly support agent for Acme Tools.",
    conversation_id="cust-8472",
)

Multi-turn conversations.

The Conversation helper reuses one conversation_id and accumulates history for you, so each turn carries the ones before it:

Python
chat = client.conversation(system_prompt="You are Acme's support assistant.")

chat.send("Hi, my order hasn't arrived.")
reply = chat.send("It's order 1234.")   # remembers the turn before it

print(reply.answer)

Handling errors.

Every exception inherits from PsiGuardError, so you can catch broadly or pick out the cases worth handling on their own. The client retries the failures that are safe to retry (connection errors and 5xx) and never retries the ones that aren't (4xx and usage caps).

Python
from psiguard import (
    Client, AuthenticationError,
    RateLimitError, UsageLimitError, PsiGuardError,
)

try:
    result = client.guard(prompt)
except AuthenticationError:
    ...   # key missing, invalid, or revoked
except RateLimitError:
    ...   # too many requests — slow down and retry shortly
except UsageLimitError as e:
    ...   # monthly cap or provider budget reached — a stop sign, not a retry
          # e.scope, e.plan, e.provider, e.used, e.limit, e.month
except PsiGuardError as e:
    ...   # anything else

Verify a key.

Python
who = client.whoami()   # runs no model, isn't metered
print(who.account)

JavaScript / TypeScript

The official Node client. Written in TypeScript with types included, works from plain JavaScript too, and has no runtime dependencies — it uses the built-in fetch. Requires Node 18+.

Terminal
npm install psiguard

Try it from your Terminal — no files needed. Paste these two lines (with your real key) and you'll see a guarded answer come back:

Terminal
export PSIGUARD_API_KEY="psg_live_your_key_here"
node -e "const {PsiGuard}=require('psiguard'); new PsiGuard().guard('What is your return policy?').then(r=>console.log(r.answer,'·',r.protection))"

Your first guarded turn — in your application. Everything from here on is program code: save it in a file (a .mjs file, say guard_test.mjs, runs as-is with plain Node) and run that file with node guard_test.mjs. With PSIGUARD_API_KEY set in your environment you can construct the client with no arguments: new PsiGuard(). Plain JavaScript works the same way with const { PsiGuard } = require("psiguard").

TypeScript
import { PsiGuard } from "psiguard";

const client = new PsiGuard({ apiKey: "psg_live_your_key_here" });

const result = await client.guard("What is your return policy?");
console.log(result.answer);      // the reply to show your user
console.log(result.protection);  // "safe" | "caution" | "intervened"

Seeing the status while you're testing

Those two console.log lines are how a real integration uses the result. But when you are the one watching the terminal, a status printed in the same plain white as everything else is easy to skim right past. formatResult renders the turn for a terminal instead:

TypeScript
import { PsiGuard, formatResult } from "psiguard";

const result = await new PsiGuard().guard("What is your return policy?");
console.log(formatResult(result));
Output
● SAFE  clean run — PsiGuard didn't need to act
│ Our return policy allows returns within 30 days of delivery...

▲ CAUTION  steered back on course mid-answer
│ I'd check with support on that specific case...

■ INTERVENED  response withheld — the answer below is a refusal
│ I'm not able to help with that.

Green dot, amber triangle, red square — so it still reads when it's colorless or the reader is colorblind. badge(result.protection) gives you the chip alone if you'd rather build your own layout.

These are display helpers, not the integration path. The client never prints, and never puts color codes inside result.answer — that string is what your user sees, and it may end up in a log, a database, or a web page, where terminal color turns into visible junk. formatResult also drops to plain text automatically when stdout isn't a real terminal (piped to a file, running in CI) and honors NO_COLOR. Requires psiguard 1.1.0 or later.

Give your assistant a personality.

TypeScript
const result = await client.guard("Do you ship to Canada?", {
  systemPrompt: "You are a friendly support agent for Acme Tools.",
  conversationId: "cust-8472",
});

Multi-turn conversations. The conversation() helper reuses one id and accumulates history for you, so each turn carries the ones before it.

TypeScript
const chat = client.conversation({ systemPrompt: "You are Acme's support assistant." });

await chat.send("Hi, my order hasn't arrived.");
const reply = await chat.send("It's order 1234.");  // remembers the turn before it

Handling errors. Everything thrown extends PsiGuardError, so you can catch broadly or pick out the cases worth handling on their own. Same behavior as the Python client: network errors and 5xx are retried; 4xx and usage caps are not.

TypeScript
import { AuthenticationError, UsageLimitError } from "psiguard";

try {
  const result = await client.guard(prompt);
} catch (e) {
  if (e instanceof AuthenticationError) {
    // key missing, invalid, or revoked
  } else if (e instanceof UsageLimitError) {
    // monthly cap or provider budget reached — a stop sign, not a retry
  }
}

Any other language

The API is plain HTTPS and JSON, so any HTTP client works without an SDK. Here's the quickstart call with fetch — and the Quickstart above shows the same thing in curl.

JavaScript · fetch (no SDK)
const resp = await fetch("https://psiguard.net/v1/guard", {
  method: "POST",
  headers: {
    "Authorization": "Bearer psg_live_your_key_here",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "What is your return policy?",
    conversation_id: "cust-8472",
  }),
});
const data = await resp.json();
console.log(data.answer, data.protection);
Security & IP

Security and intellectual property.

  • Keys are secrets. Keep them server-side, rotate them from the dashboard, and revoke any key you suspect is exposed.
  • Nothing internal crosses the wire. The API returns only your answer and a coarse protection signal. The readings and logic behind PsiGuard's decisions stay on our servers by design.
  • Proprietary technology. PsiGuard's monitoring framework is proprietary and confidential — protected as a trade secret of PsiCo, LLC. Access is licensed for use through the API; it is not licensed for reverse engineering or reproduction.
Help

Support.

Questions, odd results, or ideas? Reply to your welcome email — it comes straight to us. While PsiGuard is in active beta the API may evolve; we'll keep this page current. And a reminder worth repeating: PsiGuard is a structural monitor, not a guarantee of correctness — treat its signal as a reason to look closer, not a verdict on truth.

Read the dashboard guide