Security models, one API call away.
unbreach speaks the OpenAI chat completions format. Keep your SDK, point it at unbreach, pick a model. Every request is metered per token and nothing you send is stored.
- Base URL
https://api.unbreach.io/v1- Authentication
Authorization: Bearer ubk_live_…- Format
- OpenAI-compatible JSON, streaming over SSE
- Models
- 3 security models, from $0.15 per 1M tokens
The API is for companies. Request access for yours: we review each request within one business day. People who want to chat with unbreach use the app with a plan instead.
Quickstart
From approval to your first answer in about five minutes.
- 1
Get access
Request API access with your work email. Once we approve your organization, you get an email and the console opens Projects, API keys and Billing.
- 2
Add credits and create a key
In the console, add credits (card, bank, PayPal, crypto or wire), then API keys, Create API key. Copy the secret: it's shown once. Keep it in an environment variable, never in your code.
export UNBREACH_API_KEY="ubk_live_..." - 3
Make a request
With the OpenAI SDK, only the base URL and the key change.
# pip install openai
from openai import OpenAI
import os
client = OpenAI(
base_url="https://api.unbreach.io/v1",
api_key=os.environ["UNBREACH_API_KEY"],
)
resp = client.chat.completions.create(
model="rampart-1",
messages=[
{"role": "system", "content": "You are a SOC analyst. Answer with a verdict and next steps."},
{"role": "user", "content": "3 failed SSH logins for root from 185.220.101.4, then a success for svc-backup."},
],
)
print(resp.choices[0].message.content)
print(resp.usage) # prompt_tokens, completion_tokens: what you're billed for
// npm install openai
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.unbreach.io/v1",
apiKey: process.env.UNBREACH_API_KEY,
});
const resp = await client.chat.completions.create({
model: "rampart-1",
messages: [
{ role: "system", content: "You are a SOC analyst. Answer with a verdict and next steps." },
{ role: "user", content: "3 failed SSH logins for root from 185.220.101.4, then a success for svc-backup." },
],
});
console.log(resp.choices[0].message.content);
console.log(resp.usage); // prompt_tokens, completion_tokens: what you're billed for
curl https://api.unbreach.io/v1/chat/completions \
-H "Authorization: Bearer $UNBREACH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "rampart-1",
"messages": [
{"role": "system", "content": "You are a SOC analyst. Answer with a verdict and next steps."},
{"role": "user", "content": "3 failed SSH logins for root from 185.220.101.4, then a success for svc-backup."}
]
}'
{
"id": "chatcmpl-8f2c41",
"object": "chat.completion",
"created": 1793638800,
"model": "rampart-1",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Verdict: likely credential compromise. 185.220.101.4 is a Tor exit..."
},
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 41, "completion_tokens": 63, "total_tokens": 104 }
}
Authentication
Send your key in the Authorization header of every request. x-api-key: ubk_live_… works too, for tools that can't set a bearer token.
Authorization: Bearer ubk_live_4cYq…40 charactersKeys start with ubk_live_ followed by 40 random characters. We store only a hash of each secret: if you lose it, roll the key or create a new one.
What a key can do
Each key belongs to a project of your organization. In the console you can narrow it down:
- Models. All models, or only the ones you list. Projects can restrict models too.
- Monthly spend limit. The key stops at this amount until the 1st of next month.
- IP ranges. Requests from other addresses are refused, in addition to your organization's allowlist.
- Expiry date. After it, the key answers
expired_api_key. - Service accounts. Keys that don't belong to a person and survive team changes.
Rolling a key
Roll secret gives you a new secret right away. The previous one keeps working for 24 hours, so you can deploy without downtime. Revoking a key stops it at once.
Never ship a key in a browser, a mobile app or a public repository. Call unbreach from your server. If a key leaks, revoke it in API keys: the audit log shows who created and used it.
Chat completions
/v1/chat/completionsSends a conversation to a model and returns its answer. Same request and response as OpenAI's chat completions, so existing code and SDKs work unchanged.
Request body
modelstringrequiredThe model ID, like sentry-1, rampart-1 or bastion-1. See Models.
messagesarrayrequiredThe conversation so far. Each message has a role (system, user or assistant) and a content string. Put logs, emails or code straight into the content.
streambooleandefault falseSend the answer as it's written, as server-sent events. See Streaming.
stream_options.include_usagebooleandefault falseWith stream, adds a last chunk with the token counts. You're billed either way.
max_tokensintegeroptionalThe most tokens the answer can use. Output tokens cost more than input tokens: cap them for high-volume triage.
temperature, top_p, stop, …Other chat completion parameters go to the model as you send them.
Bodies can be up to 16 MB. The context window of each model limits how much of it the model reads.
Response
A chat.completion object. model is always the public ID you asked for, and usage holds the token counts you're billed for.
choices[0].message.contentstringThe answer.
choices[0].finish_reasonstringstop when the model finished, length when it hit max_tokens or the context window.
usageobjectprompt_tokens, completion_tokens and total_tokens.
Response headers
| Header | When | Meaning |
|---|---|---|
x-request-id | Always | Our ID for the request, like req_…. Include it when you write to us. |
retry-after | 429, 503 | Seconds to wait before trying again. |
deprecation | Deprecated model | true: the model will be retired. See Deprecations. |
sunset | Deprecated model | The date it stops answering, as an HTTP date. |
Streaming
With "stream": true, the answer arrives as server-sent events: one chat.completion.chunk per piece of text, then data: [DONE]. Show text to your analysts as it's written, or stop reading early: you pay for what was generated.
stream = client.chat.completions.create(
model="bastion-1",
messages=[{"role": "user", "content": "Reconstruct the attack chain from these logs: ..."}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices:
print(chunk.choices[0].delta.content or "", end="", flush=True)
if chunk.usage:
print("\n", chunk.usage)
const stream = await client.chat.completions.create({
model: "bastion-1",
messages: [{ role: "user", content: "Reconstruct the attack chain from these logs: ..." }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
if (chunk.usage) console.log("\n", chunk.usage);
}
curl -N https://api.unbreach.io/v1/chat/completions \
-H "Authorization: Bearer $UNBREACH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "bastion-1", "stream": true, "stream_options": {"include_usage": true},
"messages": [{"role": "user", "content": "Reconstruct the attack chain from these logs: ..."}]}'
data: {"object":"chat.completion.chunk","model":"bastion-1","choices":[{"index":0,"delta":{"content":"The attack "}}]}
data: {"object":"chat.completion.chunk","model":"bastion-1","choices":[{"index":0,"delta":{"content":"chain is: "}}]}
data: {"object":"chat.completion.chunk","model":"bastion-1","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"object":"chat.completion.chunk","model":"bastion-1","choices":[],"usage":{"prompt_tokens":812,"completion_tokens":356,"total_tokens":1168}}
data: [DONE]
The usage chunk comes only when you ask for it with include_usage. If the connection drops, we bill the tokens generated until then.
List models
/v1/modelsThe models your key can use, with their context window. Models in preview or being retired say so.
curl https://api.unbreach.io/v1/models -H "Authorization: Bearer $UNBREACH_API_KEY"{
"object": "list",
"data": [
{ "id": "sentry-1", "object": "model", "owned_by": "unbreach", "context_window": 32000 },
{ "id": "rampart-1", "object": "model", "owned_by": "unbreach", "context_window": 128000,
"status": "deprecated", "retires_at": "2027-03-01T00:00:00.000Z", "replacement": "rampart-2" },
{ "id": "rampart-2", "object": "model", "owned_by": "unbreach", "context_window": 256000, "status": "preview" }
]
}An example: status, retires_at and replacement appear only when they apply.
Errors
Errors use the OpenAI format, with an HTTP status and a stable code you can branch on. The message is written for people and can change.
{
"error": {
"message": "Rate limit reached. Retry in 12 seconds.",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null
}
}| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_request | The body isn't JSON with model and messages. | Fix the request. Don't retry. |
| 401 | invalid_api_key | The key is missing, wrong or revoked. | Check the header and the key in the console. |
| 401 | expired_api_key | The key passed its expiry date. | Create a new key. |
| 403 | organization_not_approved | Your organization's API access is still in review, or was declined. | Wait for our email, or update the request in the console. |
| 403 | organization_suspended | We suspended your organization's API access. | Write to [email protected]. |
| 403 | ip_not_allowed | The request came from an address outside the key's or the organization's allowlist. | Add the address in the console, or call from an allowed one. |
| 403 | model_not_allowed | The key or its project can't use this model. | Use an allowed model, or change the key. |
| 404 | model_not_found | No model has this ID. | Check the spelling against /v1/models. |
| 410 | model_retired | The model was retired. The message names its replacement. | Switch to the replacement. |
| 429 | rate_limit_exceeded | Too many requests per minute for this model or project. | Wait retry-after seconds, then retry. |
| 429 | insufficient_quota | Your credit balance is empty. | Add credits, or turn on auto-recharge. |
| 429 | key_limit_reached | The key reached its monthly spend limit. | Raise the limit, or wait for the 1st. |
| 429 | project_budget_reached | The project reached its monthly budget. | Raise the project budget. |
| 429 | budget_exceeded | Your organization reached its monthly budget, set to stop requests. | Raise the budget in Limits. |
| 502 | upstream_errorupstream_unavailable | The model failed or couldn't be reached. Not billed. | Retry with backoff. |
| 503 | model_unavailableendpoint_unavailable | The model or the endpoint is paused, for maintenance for example. The message says why. | Wait retry-after seconds. |
| 504 | upstream_unavailable | The model took too long to answer. Not billed. | Retry, or stream long answers. |
When the model itself refuses a request (a prompt longer than its context window, for example), you get its status and message as they are.
Models
Three sizes of the same security training. Start with rampart-1; move high-volume filtering to sentry-1 and hard investigations to bastion-1.
| Model | Best for | Context | Input | Output |
|---|---|---|---|---|
sentry-1 | Fast triage | 32K | $0.15 | $0.60 |
rampart-1 | Everyday analysis | 128K | $1.20 | $4.80 |
bastion-1 | Deep investigations | 200K | $5.00 | $20.00 |
Prices in USD per million tokens, before tax. This table shows today's prices.
Deprecations
Models move through four stages. We email the owners and admins of organizations that used a model in the last 30 days before it's retired.
Open to every organization. Behavior and price can still change: test it before you move traffic.
Generally available and stable.
Still answers, with deprecation: true and a sunset date in the headers, and a replacement in /v1/models.
Answers 410 model_retired with the name of the replacement. Gone from /v1/models.
To catch deprecations early, log a warning when a response carries the deprecation header.
Rate limits
Limits count requests per minute, per model, for your whole organization. Your usage tier sets them; the console shows yours in Limits. A project can also have its own limit, lower than the organization's.
| Model | Tier 1 | Tier 2 | Tier 3 | Tier 4 |
|---|---|---|---|---|
sentry-1 | 500 | 3,000 | 10,000 | 30,000 |
rampart-1 | 200 | 1,000 | 5,000 | 15,000 |
bastion-1 | 50 | 300 | 1,000 | 3,000 |
Requests per minute. New organizations start at tier 1. Need more? Use Request higher limits in the console.
Over the limit, you get 429 rate_limit_exceeded with a retry-after header. See Retries.
Pricing and usage
You pay per token, input and output, at the price of the model. No subscription and no minimum.
- Counted by the model. We bill the
usagethe model reports in each response. In the rare case it reports none, we estimate about 4 characters per token. - Failed requests are free. Errors before or from the model aren't billed.
- Volume discount. Monthly spend above $1,000 costs 20% less, automatically.
- Two ways to pay. Prepaid credits by default: requests stop when the balance is empty, and auto-recharge tops it up. Established companies can switch to a monthly invoice with net terms.
Example
A triage pipeline sends 2,000 alerts a day to sentry-1, about 600 input and 150 output tokens each: about $11 a month.
Usage shows in the console within seconds: per day, model, key, project and member, with a CSV export.
Spend controls
Set limits at every level, so one runaway script can't spend your month.
| Control | Where | When it's reached |
|---|---|---|
| Key spend limit | API keys, edit a key | That key gets key_limit_reached. Other keys keep working. |
| Project budget | Projects | Keys of that project get project_budget_reached. |
| Organization budget | Limits | Email alerts at the thresholds you pick (50%, 80%, 100%). Set to stop, every key gets budget_exceeded. |
| Low balance alert | Billing | Owners and billing members get an email. |
Every limit resets on the 1st of the month, in UTC.
Data and security
Zero data retention
Request and response bodies are never logged or stored, and never used for training. We keep only token counts for billing.
Hashed keys
We store a SHA-256 hash of each secret, never the secret. Rolling and revoking take effect at once.
IP allowlists
Per key and per organization, IPv4 and IPv6, with CIDR ranges.
Team controls
Roles, SSO with SAML or OIDC, SCIM provisioning, required two-factor authentication and an audit log.
Read the privacy policy for the details, or ask for our security documentation.
Retries
Retry 429 and 5xx answers, waiting longer each time and honoring retry-after. Never retry other 4xx errors: the request itself needs fixing. The OpenAI SDKs already retry twice; raise it for batch jobs.
client = OpenAI(
base_url="https://api.unbreach.io/v1",
api_key=os.environ["UNBREACH_API_KEY"],
max_retries=5, # 429 and 5xx, with backoff and retry-after
timeout=120,
)
const client = new OpenAI({
baseURL: "https://api.unbreach.io/v1",
apiKey: process.env.UNBREACH_API_KEY,
maxRetries: 5, // 429 and 5xx, with backoff and retry-after
timeout: 120_000,
});
# curl retries 429 and 5xx, and honors Retry-After
curl --retry 5 --retry-max-time 120 https://api.unbreach.io/v1/chat/completions \
-H "Authorization: Bearer $UNBREACH_API_KEY" -H "Content-Type: application/json" \
-d '{"model": "sentry-1", "messages": [{"role": "user", "content": "..."}]}'
Going to production
- One key per service and environment, in its own project, so you can see and cap each one.
- Keys in a secret manager, never in code or front ends. Restrict them by IP when your servers have fixed addresses.
- Budgets and alerts on the organization and on each project.
- Auto-recharge, or monthly invoicing, so production never stops on an empty balance.
- Retries on 429 and 5xx, and a timeout that fits your longest answers.
- Log
x-request-idwith your own request IDs, and watch for thedeprecationheader.
Ready to build?
Request access for your company. We answer within one business day.