Developer documentation. Three ways in, one credential, and one idea running through all of them: nothing identifying reaches a model.
Base URL
https://secureai.one/v1
The Worker's own hostname, https://secure-ai-worker.secureai-one.workers.dev, answers the same routes and always will. The address above is proxied to it so that examples read as one domain.
Five minutes: switch your app in one line
Already calling OpenAI or Anthropic? Keep your code. Change where it points, and every request goes through Secure AI: names, emails, phone numbers and account numbers are swapped for stand-ins before the model sees them, and put back in the answer you receive.
1. Get a key. Sign in at secureai.one, open the developer console, and create one. 50,000 requests a month are free.
2. Change one line.
With your own model key (you keep your vendor contract; we never store the key):
python
from openai import OpenAI
client = OpenAI(api_key="sai_...:sk-...", base_url="https://secureai.one/v1/proxy")
ts
import OpenAI from "openai";
const client = new OpenAI({ apiKey: "sai_...:sk-...", baseURL: "https://secureai.one/v1/proxy" });
With our models (no vendor account needed, billed to your balance):
reply = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Draft a reply to Sara Whitfield on 07700 900123"}],
)
print(reply.choices[0].message.content) # Sara's real name and number, back in place
The model received a stand-in name and number. Your code received the real ones. Streaming, tool calls and errors all keep their usual shape, so retries and error handling you already have keep working.
4. See what was protected.Home in the app counts the details hidden in your requests this month, by kind. Rules decides what your agents' tool calls may send.
Sixty seconds, assuming you have a key from the developer console.
Take the people out of a piece of text:
curl https://secureai.one/v1/redact \
-H "Authorization: Bearer sai_..." \
-H "Content-Type: application/json" \
-d '{"text": "Email Sara Whitfield at sara@example.com"}'
{ "object": "restoration", "text": "I emailed Sara Whitfield.", "restored": 1 }
That is the whole product. Everything below is detail.
If you would rather not hold the map or make two calls, the proxy does all three steps in one request using your own model key — see `/v1/proxy/chat/completions`.
Which one you want
You want
Use
Whose model
To keep your existing AI provider, and change one line
/v1/proxy/chat/completions
Yours
To control exactly what goes where
/v1/redact + /v1/restore
Yours
To not have a provider at all, or to run an agent's tool calls through the privacy layer
/v1/chat/completions
Ours
The same, from the Claude SDKs or Claude Code
/v1/messages
Ours (or yours on /v1/proxy/messages)
Nobody but the model to see the conversation — not us, not our cloud
The first is what most people should use. The second is the same work with the seams exposed. The third is for people who would rather buy the whole thing.
Authentication
Every endpoint takes an API key, created in the app under Settings → Developer.
Authorization: Bearer sai_...
The key is shown once when you create it. What we store is its SHA-256, so it cannot be shown again or recovered — if you lose it, revoke it and make another. A key acts for the account that created it, carries that account's plan, and can be revoked on its own without touching the others.
A request with no key, or with something that is not a key, gets 401. That includes a valid sign-in token: tokens are for the apps, this door takes keys.
API access
Every signed-in account can make a key and use the API — no request, no approval. Open since 2026-09-24.
Free: 50,000 requests a month. Past that, 402 with code free_limit
until the month turns over or the account subscribes. No balance, no overage.
Subscription ($49 a month): 50,000 requests a month, then overage (below).
The subscription is bought separately from the app plans. It is a switch on the account, not a stamp on the key: a lapsed subscription drops the account back to the free allowance with its keys in place, and renewing restores the full allowance with nothing to re-issue.
What the subscription includes
50,000 requests a month. Past that, $0.001 per 1,000 characters of each request (one unit at least), taken from a balance you top up in advance.
Every call to /v1/redact, /v1/restore and the proxy counts as one request — restore included, because otherwise the two-call integration would be the cheap way to use the one-call product.
Going over is allowed rather than blocked. What refuses is going over with no balance to cover it: 402, code insufficient_balance. GET /v1/usage shows how much of the month is gone, so a job can check before it starts.
Nothing is charged inside the allowance, so if you never go over you never need a balance at all.
/v1/proxy/chat/completions
Redact, call your model with your key, restore the answer. One request.
You keep your vendor contract, your rates and your own logs. We never see the bill, because it is not ours — your provider key is forwarded to the one call that needs it and is never stored, logged or written down.
Request
POST /v1/proxy/chat/completions
Authorization: Bearer sai_... your Secure AI key
X-Provider-Key: sk-ant-... your model vendor's key
X-Provider: anthropic optional; only when we cannot tell
X-Provider-Base-URL: https://... optional; any OpenAI-compatible server
X-Provider-Region: us-east-1 Bedrock only
Content-Type: application/json
The body is an ordinary OpenAI chat completion request.
json
{
"model": "claude-sonnet-5",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Draft a reply to Sara Whitfield on 07700 900123" }
],
"stream": false
}
model is required and must be one your own key can call — there is no automatic model choice here, because choosing a model is a decision about cost and the cost is yours. The vendor is worked out from the model name where we recognise it; send X-Provider when we do not.
For the first three, X-Provider is only needed when we do not recognise the model name. For every other vendor, send it.
Any OpenAI-compatible server (vLLM, Ollama or LM Studio behind a public https address, a vendor not in the table, a regional endpoint of one that is): send X-Provider-Base-URL with the address that comes before /chat/completions. Your key goes to it as Authorization: Bearer <key>. The address must be https:// on the standard port, a public host name (not an IP address, localhost or an internal name), and not one of ours. Redirects are not followed.
Azure OpenAI uses Azure's v1 API. Your key is sent as api-key, and model is the name you gave the deployment. The address must be an Azure OpenAI one (*.openai.azure.com, *.cognitiveservices.azure.com or *.services.ai.azure.com).
Amazon Bedrock uses the Converse API. X-Provider-Key takes either a Bedrock API key, or an access key pair written ACCESS_KEY_ID:SECRET_ACCESS_KEY (add :SESSION_TOKEN for temporary credentials). A key pair signs the request (AWS Signature Version 4) and is never sent anywhere itself. Streaming uses converse-stream, and tool calls work as they do for every other vendor.
usage is your vendor's own count, passed through. We do not bill on it.
Streaming works the same way: set "stream": true and read server-sent events in OpenAI's format. The stand-ins are put back inside the stream, so no chunk you receive ever contains one.
With the OpenAI SDK
python
from openai import OpenAI
client = OpenAI(
api_key="sai_...",
base_url="https://secureai.one/v1/proxy",
default_headers={"X-Provider-Key": "sk-ant-..."},
)
reply = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Draft a reply to Sara on 07700 900123"}],
)
Nothing else in your code changes.
One key field only (n8n and similar tools)
Some tools give a connection just a base URL and an API key, with no room for a second header. Put both keys in the key field, separated by a colon:
sai_...:sk-ant-...
The first half is ours and authenticates you; the second is your vendor's and is handled exactly like X-Provider-Key: forwarded to the one call that needs it, never stored.
In n8n: create an OpenAI credential, set Base URL to https://secureai.one/v1/proxy, and put sai_...:<your vendor key> in API Key. Then use it in an OpenAI Chat Model node and pick a model.
Choosing the vendor without a header. With no room for X-Provider, put the vendor in front of the model name. It is read only when no X-Provider or X-Provider-Base-URL header was sent, and only for vendors in the table above:
model
Goes to
mistral/mistral-large-latest
Mistral, model mistral-large-latest
grok/grok-4
xAI, model grok-4
openrouter/anthropic/claude-sonnet-5
OpenRouter, model anthropic/claude-sonnet-5
azure/<resource>/<deployment>
https://<resource>.openai.azure.com, that deployment
bedrock/<region>/<model id>
Bedrock in that region
GET /v1/proxy/models and GET /v1/models
The model list OpenAI clients ask for first, which n8n uses for its model dropdown and its credential test. /v1/proxy/models lists the models the proxy recognises by name; /v1/models lists what /v1/chat/completions runs, the secureai-* routes first. Both take your key in either form and answer in OpenAI's { "object": "list", "data": [...] } shape.
/v1/redact and /v1/restore
The same work, in two calls, with your model call in the middle. Use this when you want to see and control exactly what leaves your systems.
Redact
POST /v1/redact
Authorization: Bearer sai_...
Content-Type: application/json
json
{ "text": "Hi, I am Sara Whitfield. Ring me on 07700 900123 or email sara@example.com" }
Response:
json
{
"object": "redaction",
"cleaned_text": "Hi, I am Sylvie Nakashima. Ring me on (415) 637-6526 or email ingrid.sinclair@outlook.com",
"text": "Hi, I am Sylvie Nakashima. Ring me on (415) 637-6526 or email ingrid.sinclair@outlook.com",
"entities_found": { "name": 1, "phone": 1, "email": 1 },
"map": {
"Sylvie Nakashima": "Sara Whitfield",
"(415) 637-6526": "07700 900123",
"ingrid.sinclair@outlook.com": "sara@example.com"
},
"names_decided": true,
"redacted": 3
}
cleaned_text and text are the same; text is the original name and stays. entities_found counts what was hidden, by kind.
The stand-ins are believable people rather than [REDACTED] markers, on purpose: a model given [NAME] writes about [NAME], and the answer comes back needing repair. A model given a name writes normally.
map is keyed by stand-in and valued by the real thing. redacted counts the entries; names_decided says whether the name detection actually ran.
Optional fields: map, allow, profile, strict — as above — and organisations: true hides the names of companies and other private organisations as well, each behind an invented company name ("Bellmont Holdings"). Off by default, because a company is usually what the question is about. Courts, ministries and EU institutions stay as written. "all" hides those too, for a document anonymised in full (a judgment, a case file); teams, job titles and document names stay as written.
medical: true also removes what HIPAA's Safe Harbor method lists and an ordinary request leaves: every date except its year (admission, discharge, visit, test, death), ages over 89 and a birth year that gives one away, towns, counties and ZIP codes, the names of hospitals and clinics, every web address and IP address, and record numbers under a clinical label (accession, specimen, device serial). Diagnoses, medicines, doses, results, times and years stay, so the record is still worth asking about. Dates are moved, not scrambled: every date in a conversation moves by the same whole number of weeks within its year, so a stay of seven days is still seven days and a Tuesday is still a Tuesday. Off by default. If the redaction service cannot run medical mode, the request is refused with a 503 rather than answered without it.
It reads UK and European records the same way: dates written "14. März 2024", "12 de enero de 2025" or "5 avril" (moved and written back in their own language), ages written "93-jährige", "91 ans" or "94 años", hospitals written kind-first ("Klinikum Schwabing", "CHU de Rennes", "Ospedale San Raffaele"), UK postcodes, and European postcodes before their town ("50667 Köln").
Medical mode removes the identifiers; whether a use is permitted under HIPAA is a matter for your agreements and your compliance team. Under the GDPR, data with its identifiers replaced is still pseudonymised personal data, not anonymous: it lowers the risk, and the rest of the regulation still applies.
spans: true also returns where each hidden value was and what kind it was, for highlighting or for scoring against a labelled test set:
Offsets count Unicode code points, as Python does, and end is exclusive. A value is listed at every place it stands as a whole word. type is one of name, organisation, email, phone, address, postcode, card, iban, ssn, govid, host and secret, and with medical also date, age, place and the other medical kinds.
confidence is how often a value of that kind was really a private detail on the public test sets (public-benchmarks/), from 0 to 1. A name of two words or more is 0.96, a single word 0.83, and a value with digits in it that was hidden as a name 0.47, so a reviewer can look at the low ones first. It is measured per kind, not computed per value, and it is null for kinds not yet measured (organisations and the medical kinds).
risk: true also returns what is still visible in the cleaned text that could point to one person: a date of birth, other dates, an age, a town, a place of care, a job few people have, a person's sex. No one of them identifies anybody; together they can.
score is 0 to 100: low under 20, high from 50. Each kind counts once, and the score is 1 − Π(1 − weight). The weights are a judgement, not a measurement, so read the score as a checklist with a number on it: the reasons say what to hide next, and medical: true hides dates, towns and ages over 89. A low score means fewer known risks, not none.
Restore
Restore on your own machine. The SDKs put the real values back locally (sai.restoreLocal(reply, map) in TypeScript, sai.restore(reply, r.map) in Python), with the same code this endpoint runs, so the map never has to come back to us. That includes a model using half a stand-in name ("Hi Sylvie" for "Sylvie Nakashima"). The endpoint below stays for callers without an SDK.
Send the model's answer back with the same map.
POST /v1/restore
json
{
"text": "I will ring Sylvie Nakashima on (415) 637-6526.",
"map": { "Sylvie Nakashima": "Sara Whitfield", "(415) 637-6526": "07700 900123" }
}
json
{
"object": "restoration",
"text": "I will ring Sara Whitfield on 07700 900123.",
"restored": 2
}
An empty map is not an error: text with nothing personal in it round-trips through both endpoints unchanged, and your code should not have to branch on that.
We do not keep the map
The map goes back with the response and is stored nowhere. That is a deliberate limit rather than an omission: keeping it would mean holding, for every customer, exactly the table that turns redacted text back into real people — which is the database this product exists in order not to be.
The consequence is yours to handle. If you lose a map, that answer cannot be restored. Hold it for the life of the conversation, in your own system, wherever you already keep the conversation itself.
This is also the argument for the proxy endpoint, where there is no map to lose.
/v1/chat/completions
Our models, our provider accounts, metered against a balance you buy in advance. Same OpenAI shape as the proxy, without the X-Provider-Key header.
model may be omitted, in which case a model is chosen by how hard the question is. Named routes are secureai-auto, secureai-plus, secureai-pro and secureai-ultra.
Spending is checked before the call and settled after: the most a request could cost is held, and the difference is released once the answer is in. A balance cannot go negative, so a runaway loop stops rather than arrives as an invoice.
Tool calls
Agent frameworks — the OpenAI Agents SDK, LangChain, the Vercel AI SDK, n8n's AI nodes — send tools, expect tool_calls back, and answer each one with a role: "tool" message. This endpoint speaks that conversation, so an agent moves over by changing its base URL.
python
reply = client.chat.completions.create(
model="secureai-auto",
messages=[{"role": "user", "content": "Email Sara Whitfield her invoice"}],
tools=[{
"type": "function",
"function": {
"name": "send_email",
"description": "Send an email",
"parameters": {
"type": "object",
"properties": {"to": {"type": "string"}, "body": {"type": "string"}},
},
},
}],
)
call = reply.choices[0].message.tool_calls[0]
# call.function.arguments has the real address, not a stand-in
What happens on the way:
The whole conversation is hidden first. Your messages, the model's
earlier tool calls and every tool result are redacted before the model reads them. A customer record a tool returns is the likeliest place for private details to enter an agent's conversation, so it gets the same treatment as anything you type.
Arguments come back real. The model writes calls against stand-ins; the
real values are put back into arguments before you receive them, escaped as JSON, so your tool acts on the real customer.
Your policy decides each call. A call your policy blocks
— a card number on its way out, a tool the account has banned — is withheld: you don't receive it, and the reply says why. A call your policy marks approve is held as an approval; once a person approves it, the same call goes out on the next request. Either way the response carries secureai.withheld, listing what was held and why.
Every call is in the audit trail, as kinds and locations, never values.
tool_choice takes "auto", "none", "required" or a named function. Streaming works; each tool call arrives whole, in one chunk, rather than as fragments of JSON, so a stand-in can never reach you half-restored.
Name a GPT or Gemini model from GET /v1/models and the conversation goes to that vendor; otherwise it runs on our most capable model. With your own key, the same works on /v1/proxy/chat/completions (and /v1/proxy/messages), on the model you name, billed per request like everything else there. Tool conversations are text only for now: an image in one is refused with 400 rather than dropped.
Authenticated with the API key, so a long job can check its own balance before starting.
Anthropic's format: /v1/messages
Everything above also answers in Anthropic's Messages format, for the Claude SDKs, Claude Code and anything built on them. Same redaction, same models, same tool calls, policy and billing — only the shape of the request and the answer change.
Anthropic path
Same as
POST /v1/messages
/v1/chat/completions (our models)
POST /v1/proxy/messages
/v1/proxy/chat/completions (your key)
The key can go in x-api-key, as the SDKs send it, or in Authorization.
python
import anthropic
client = anthropic.Anthropic(api_key="sai_...", base_url="https://secureai.one")
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Draft a reply to Sara Whitfield on 07700 900123"}],
)
With your own Anthropic key, point the SDK at the proxy and put both keys in the one field:
Claude Code: set ANTHROPIC_BASE_URL=https://secureai.one and ANTHROPIC_API_KEY=sai_....
tools, tool_use and tool_result work as they do in Anthropic's API, on /v1/messages: the conversation is hidden first, tool_use inputs come back with the real values, and your policy can withhold or hold a call — see Tool calls. Streaming sends Anthropic's events; a tool call arrives in one input_json_delta. A model name we don't offer is answered by secureai-auto rather than refused, so a client configured for Anthropic works unchanged.
Secure mode
Everything above is redacted on our servers, which means our servers hold the text for as long as that takes. Secure mode removes that. The conversation is sealed in your process to a key that exists only inside an AWS Nitro enclave, after your client checks a document signed by AWS's hardware that says which program holds the key. We relay the envelope without being able to open it. The enclave hides the people, asks the model, puts them back, and seals the answer to you.
If the enclave cannot prove what it is running, your client sends nothing. There is no fallback to the readable path.
Part of the API subscription. Not on the free allowance.
The TypeScript and Swift SDKs do the verifying, sealing and opening (sai.secureChat(...) in both). To decide for yourself which enclave builds may read your users' messages, pin them: new SecureAI({ apiKey, trustedImages: ["58ecfbe3…"] }).
What it costs
A fixed price per message, charged up front: the model's own rate for the size of what you sent, plus 4,096 tokens of answer. The token counts are inside the envelope where we cannot read them, so there is nothing to settle afterwards. Nothing is charged if the enclave does not answer. The charge is in the X-Secure-AI-Charged-Usd response header.
GET /v1/sealed
No key needed. What a client must know before it can seal anything:
images are the PCR0 values of the enclave builds we vouch for. Fetch <enclave>/attestation?nonce=… and verify the document against them and the AWS Nitro root.
The body is the sealed envelope { "v": 1, "k": "…", "c": "…" }: a fresh AES-256-GCM key wrapped to the enclave's RSA-OAEP-SHA-256 key, and the request encrypted under it. The answer is a stream of sealed pieces, each bound to its position so a piece moved or a stream cut short fails to open. Building this by hand is possible and not recommended; the SDK's src/secure.ts is the reference.
Status
Means
400
model_not_available: not a model GET /v1/sealed lists
402
Out of balance
403
subscription_required
411 / 413
No Content-Length, or too large
503
The enclave is not reachable. Nothing was sent or charged
Numbers that do not say what they are
A card number passes a checksum and an IBAN has a shape, so rules find them. So does a number the sentence names: account 40718842, SSN: 756.9972.1808.
Some numbers carry no label at all:
text
Payment will be made from 10972500.
Emergency supplies in 16356 need a refill.
The first is an account number and the second a postcode, but nothing in either sentence says so, and a rule that removed every five-digit number would also remove years, prices and order references. So any number the rules could not place is read in its sentence by the same model that finds names, and swapped out when it identifies a person, an account or a place.
This runs on /v1/redact, /v1/proxy/chat/completions (and so n8n) and chat alike. It only costs anything when a message has a number the rules left alone.
Strict mode
Identifying detail is found in two ways. Structured things — phone numbers, emails, addresses, card numbers — are found by rules that always work. Names belonging to other people, and numbers with no label, are found by a model, and that check is best-effort: it can be slow, and it can return something unparseable.
By default, a failed model check does not stop the request. The structured stages have already run, and refusing to answer in order to protect a name that may not be there is its own kind of harm.
"strict": true asks for the other trade. When the name check or the number check cannot decide, the request is refused with 503 and nothing is sent to any model. It never fires when a check decided there was nothing to find.
If you are pushing customer records through this rather than one person's messages, you probably want strict mode on.
Tell us what it missed
POST /v1/feedback
When the scanner lets something through, or redacts something it should not have, report it. A fix made from your report reaches every account, not only yours.
miss (it should have been found) or false_positive (it should have been left alone)
value
The text in question, 2 to 200 characters
context
Optional. The sentence it was in. The word in front of the value is often the whole difference: account 40718842 and order 40718842
Up to 25 at once as { "reports": [ ... ] }.
What is kept. Only the shape: which way it was wrong, what kind of characters (digits, capitalised words, has an @), a length band, and the word in front of it if that word is on a short fixed list. The value and the sentence are read once to work that out and then dropped. They are not stored or logged. The answer lists signals, which is everything that was kept, so you can check.
counted falls below received once an account has sent 200 in a day. The limit is there so one script cannot decide what gets fixed next.
To stop a false positive on your own account straight away, use POST /v1/policy/allow as well. That changes your account now, and the report fixes the scanner later.
Errors
Errors come back in OpenAI's shape, so an SDK unwraps them without help.
json
{
"error": {
"message": "Not enough API balance. $0.00 left; top up to keep going.",
"type": "insufficient_quota",
"code": "insufficient_balance",
"param": null
}
}
Status
Means
400
The request was wrong — no model named, no provider key, a body that is not JSON, or a provider address that is refused (invalid_provider)
401
No API key, or one that is not recognised
402
The free allowance is used up (free_limit), or a subscriber is out of balance (insufficient_balance). The code says which
403
Your plan does not include that model, or your account's Rules block a kind of detail in the message (blocked_by_rules; on the drop-in, and on /v1/redact from the plugin or keyboard)
413
Too much text in one request
429
A rate limit, or a spending cap reached
503
Redaction could not run, so nothing was sent
The status is left alone deliberately. Your SDK's retry policy is built on it, and softening a 429 into something friendlier turns a backoff into a hammering.
On the proxy endpoint, a refusal from your vendor is passed back with its own status and a message saying whose refusal it is — a bad key or an exhausted quota is an answer about your account, not about ours.
Limits
Text per /v1/redact call
200,000 characters
Input per chat request
16k–96k tokens, by plan
Requests
60 a minute, per key, per location
Included requests
50,000 a month, then $0.001 per 1,000 characters
Keys per account
20
Agents: the control plane
Everything above works on text. This is the half that works on actions: an agent is about to call a tool, and Secure AI decides whether it may, before it does.
The response carries a decision of allow, redact, approve or block; the rewritten input when it is not blocked; the map; and findings, with the kind and location of everything found. A blocked response carries no input, so there is nothing to send by mistake.
approve means a person has to decide. The response carries an approvalId and an expiresAt and nothing sendable. Poll GET /v1/approvals/<id>, then call /v1/inspect again with the same input and that approvalId. Approvals are single use, expire into a refusal rather than a release, and only cover the action they were granted for.
Policy
Rules live on the account, not in the request, so an agent cannot set its own.
Endpoint
What it does
GET /v1/policy
The rules in force
PUT /v1/policy
Replace them. A rule looks like { "kind": "card", "decision": "block", "direction": "outbound" }; the most severe decision across an action's findings wins. Also takes a webhook to call when something is refused
POST /v1/policy/allow
Stop flagging a value, such as @yourcompany.com, for every agent on the account
GET /v1/policy/templates
Starting points: balanced, PCI, HIPAA, GDPR, client confidentiality, nothing-leaves. A template sets up one control; it is not compliance on its own
POST /v1/policy/templates
Apply one by id, keeping your allowlist, gateway destinations and webhook
Approvals and the audit trail
Endpoint
What it does
GET /v1/approvals
Actions held for a person, newest first. ?status=pending for the queue
POST /v1/approvals/<id>
Approve or deny: { "decision": "approved" } or "denied", with an optional note. Must be a signed-in person: an API key cannot approve its own agent's action
GET /v1/audit
What agents did, newest first. Recorded as kinds and locations, never as the values
GET /v1/audit/summary
Counts over a recent window
GET /v1/audit/suggestions
Rules the trail suggests, each with its evidence. Offered, never applied on their own
/v1/gateway/<destination-url>
The same inspection with no code to write. Point an HTTP client at the gateway with the real destination after the prefix, and every request is checked on the way past. Put the destination's own credential in X-Secure-AI-Forward-Authorization; the Authorization header is ours and is never forwarded. Answers carry X-Secure-AI-Decision and X-Secure-AI-Audit-Id. A refusal is a 403, and nothing reaches the destination.
SDKs
TypeScript and Python for servers and agents; Swift and Kotlin for iPhone and Android apps, which add private chat. The same shape in all four.
Swift: add https://github.com/secureaione-jpg/secure-ai-sdk in Xcode (File → Add Package Dependencies), from 0.1.0. Kotlin, from JitPack: implementation("com.github.secureaione-jpg:secure-ai-sdk:0.1.0").
guard wraps a function your agent already calls and calls it with the rewritten arguments, so protection does not depend on remembering to check a decision at every call site. If the policy refuses, the function is never called and ActionBlocked is raised.
ts
import { SecureAI } from "@secure-ai/guard";
const sai = new SecureAI({ apiKey: process.env.SECURE_AI_KEY!, agent: "support-bot" });
const sendEmail = sai.guard("email.send", (input) => mailer.send(input));
python
from secure_ai import SecureAI
sai = SecureAI(api_key=os.environ["SECURE_AI_KEY"], agent="support-bot")
send_email = sai.guard("email.send", mailer.send)
tools = sai.guard_tools(agent.tools) # LangChain and similar, in one line
Secure mode, where we cannot read the conversation at all, is sai.secureChat(...) in TypeScript and Swift. See Secure mode.
Found a miss? sai.reportMiss(value, { context }) in TypeScript and sai.report_miss(value, context=...) in Python. reportFalsePositive and report_false_positive cover the other direction. See Tell us what it missed.
Agent frameworks
Each of these keeps its own model and your own model key. Secure AI sits in between: names, numbers and accounts are hidden before the model sees them, tool results are hidden the same way, and each tool call comes back with the real values in it, checked against your Rules first. Get a key on the quick start.
OpenAI Agents SDK (Python)
Give the SDK a client pointed at the drop-in, and use the Chat Completions API, which is the one the drop-in speaks.
python
from openai import AsyncOpenAI
from agents import Agent, Runner, set_default_openai_api, set_default_openai_client, set_tracing_disabled
set_default_openai_client(AsyncOpenAI(
api_key="sai_...", # your Secure AI key
base_url="https://secureai.one/v1/proxy",
default_headers={"X-Provider-Key": "sk-..."}, # your OpenAI key
))
set_default_openai_api("chat_completions")
set_tracing_disabled(True)
agent = Agent(name="Support", instructions="Answer the customer.", model="gpt-5.5")
result = Runner.run_sync(agent, "Write to Sara Whitfield about her refund")
print(result.final_output) # Sara's real name is back in it
Turn tracing off, or give it its own key with set_tracing_export_api_key: by default the SDK sends traces to OpenAI using the client's key, which here is your Secure AI key, and the traces would hold the conversation.
Another vendor's model works the same way: add "X-Provider": "mistral" (or any name in Which providers) to the headers and use that vendor's key.
Claude Agent SDK and Claude Code
Both read two environment variables. Point the base URL at the drop-in and put both keys in the API key, separated by a colon:
from claude_agent_sdk import query
async for message in query(prompt="Draft a reply to Sara Whitfield about her refund"):
print(message)
The SDK speaks Anthropic's Messages format; the drop-in answers it at /v1/proxy/v1/messages (see Anthropic's format). Your Anthropic key is passed on to the one call that needs it and never stored.
Vercel AI SDK
ts
import { generateText } from "ai";
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
const secure = createOpenAICompatible({
name: "secure-ai",
baseURL: "https://secureai.one/v1/proxy",
apiKey: process.env.SECURE_AI_KEY, // sai_...
headers: { "X-Provider-Key": process.env.OPENAI_API_KEY! }, // your model key
});
const { text } = await generateText({ model: secure("gpt-5.5"), prompt: "Write to Sara Whitfield" });
For LangChain-style tool lists, sai.guard_tools(tools) in the Python SDK holds or refuses a tool call that would send a card or a password, by your Rules (see SDKs).
Anything else
Any OpenAI-compatible client works by changing its base URL to https://secureai.one/v1/proxy and adding the X-Provider-Key header; any Anthropic client by setting its base URL to the same address and its key to sai_...:sk-ant-....
Every example in this chapter was run against the live drop-in on 2026-10-09: the OpenAI Agents SDK 0.23, the Claude Agent SDK, the Vercel AI SDK, LangChain in Python and JavaScript, and the OpenAI and Anthropic SDKs.
Use it from an assistant
There is an MCP server, so Claude, Cursor or an agent you wrote yourself can use the redaction as a tool without anybody writing the calls.
https://secureai.one/mcp
Easiest: connect with your account. In Claude, open Settings → Connectors → Add custom connector, and give it https://secureai.one/mcp; in ChatGPT, add it as a connector the same way. Press Connect, sign in to Secure AI and approve. The assistant gets its own key, named after it, which you can revoke in the developer console.
Or use a key. Add it as a remote MCP server with your key as a bearer token. In a client that takes JSON:
It offers two tools, redact and restore, which are the two endpoints above. The assistant decides when to reach for them; the instructions it gets tell it to redact before sending anything onward and to keep the map for the reply.
Tool calls count against the same monthly allowance as everything else.
language model reads, for anybody asking an assistant what this does.
Keys
Created, rotated and revoked in the developer console, which also shows requests this month by key.
A key may carry an optional monthly spending limit. It is not a second balance — the balance is the money — it is a wall for the key going into something still being written, so one bad loop cannot spend a month of it in an afternoon. Leave it empty for the key your own service runs on: the balance already bounds it, and a limit nobody chose refusing a legitimate job at three in the morning is the worse failure.
Revoking takes effect immediately and cannot be undone. Make another.
Rotating gives you a new key with the same name and limit. The old key keeps working for 24 hours and then stops, so whatever runs on it can switch over without an outage. The new key is shown once.