Curbi / API · v2026-11-01
Your agent’s reply.
One structured check.
Send a reply and your policy. Get pass/fail, the exact spans that broke a rule, and why. Deterministic, no model call, no API key during public access.
Run your first API check
Send the example reply and policy. The response flags the $99 price against the $49 policy.
Open live API tester →curl https://api.curbi.io/v1/check \
-H 'Content-Type: application/json' \
--data '{"policy":{"prices":[{"item":"Starter","amount":49}]},"reply":"Starter costs $99."}'{
"mcpServers": {
"curbi": {
"url": "https://api.curbi.io/mcp"
}
}
}# pip install curbi-langchain
from curbi_langchain import curbi_guard
policy = {"prices": [{"item": "Starter", "amount": 49}]}
guard = curbi_guard(policy=policy)
guard.invoke("Starter costs $99.") # Raises on the price mismatch.One request, any language.
The example intentionally contains a wrong price. Every tab is executed in Curbi’s test suite: the HTTP and MCP ones against the real Worker, the AI SDK one against the real middleware with a stand-in model.
curl https://api.curbi.io/v1/check \
-H 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"version": "2026-11-01",
"policy": {
"prices": [
{
"item": "Starter",
"amount": 49
}
]
},
"reply": "Starter is $99 a month."
}
JSONconst res = await fetch('https://api.curbi.io/v1/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({"version":"2026-11-01","policy":{"prices":[{"item":"Starter","amount":49}]},"reply":"Starter is $99 a month."}),
});
const result = await res.json();
console.log(result.pass, result.findings.map(f => f.rule));import json, urllib.request # standard library only
req = urllib.request.Request(
"https://api.curbi.io/v1/check",
data=json.dumps({"version":"2026-11-01","policy":{"prices":[{"item":"Starter","amount":49}]},"reply":"Starter is $99 a month."}).encode(),
headers={"Content-Type": "application/json", "User-Agent": "curbi-quickstart/1.0"}, # the default urllib agent can be refused at the edge
)
result = json.load(urllib.request.urlopen(req))
print(result["pass"], [f["rule"] for f in result["findings"]])curl -s https://api.curbi.io/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curbi-curl","version":"1.0.0"}}}' > /dev/null
curl -s https://api.curbi.io/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s https://api.curbi.io/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-06-18' \
--data-binary @- <<'JSON'
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "policy_check",
"arguments": {
"version": "2026-11-01",
"policy": {
"prices": [
{
"item": "Starter",
"amount": 49
}
]
},
"reply": "Starter is $99 a month."
}
}
}
JSON// Check every model reply in-process (source preview: revenue-engine/connectors)
import { wrapLanguageModel } from 'ai';
import { curbiMiddleware } from './connectors/src/ai-sdk.mjs';
const model = wrapLanguageModel({
model: yourModel,
middleware: curbiMiddleware({
policy: {"prices":[{"item":"Starter","amount":49}]},
fallback: 'A teammate will confirm that price.',
}),
});LIVE CHECK API
Check a reply.
Edit the example, run it, inspect the finding. This sends the reply to Curbi’s public API.
Policy used in this example
Starter costs $49. A refund claim requires a matching completed refund record. This example supplies no record.
Edit your own policy →One reply. No signup. No model inference.
Run a check to inspect the JSON response.
A pass means no configured rule fired. Supplied records are unverified; this check grants no execution authority or independent provider confirmation.
Recipes, with real responses.
Each response below was produced by running the shipped checker on the request shown, at build time. Change the request, and the documented response changes with it.
Catch a wrong pricecaught
Request
{
"version": "2026-11-01",
"policy": {
"prices": [
{
"item": "Starter",
"amount": 49
}
]
},
"reply": "Starter is $99 a month."
}Response from the checker
{
"object": "check",
"version": "2026-11-01",
"recordTrust": "caller_supplied_unverified",
"pass": false,
"findings": [
{
"rule": "price:mismatch",
"span": [
11,
14
],
"text": "$99",
"why": "Starter is $49, not $99."
}
],
"proof": {
"scope": "operation_and_subject_only",
"factsAuthenticated": false,
"replayProtection": "within_request_only",
"authorizationGranted": false
}
}Catch a refund claim with no recordcaught
Request
{
"version": "2026-11-01",
"policy": {
"actions": {
"refunded": "billing.refund"
}
},
"reply": "I have refunded your order."
}Response from the checker
{
"object": "check",
"version": "2026-11-01",
"recordTrust": "caller_supplied_unverified",
"pass": false,
"findings": [
{
"rule": "action:unrecorded",
"span": [
0,
15
],
"text": "I have refunded",
"why": "Says \"I have refunded\" but no billing.refund effect is DONE in the record."
}
],
"proof": {
"scope": "operation_and_subject_only",
"factsAuthenticated": false,
"replayProtection": "within_request_only",
"authorizationGranted": false
}
}Pass a refund claim that has a recordpass
Request
{
"version": "2026-11-01",
"policy": {
"actions": {
"refunded": "billing.refund"
}
},
"reply": "I have refunded your order.",
"record": {
"effects": [
{
"operation": "billing.refund",
"status": "DONE"
}
]
}
}Response from the checker
{
"object": "check",
"version": "2026-11-01",
"recordTrust": "caller_supplied_unverified",
"pass": true,
"findings": [],
"proof": {
"scope": "operation_and_subject_only",
"factsAuthenticated": false,
"replayProtection": "within_request_only",
"authorizationGranted": false
}
}Check a batch in one requestcaught
Request
{
"version": "2026-11-01",
"policy": {
"prices": [
{
"item": "Pro",
"amounts": [
49,
490
]
}
]
},
"cases": [
{
"id": "a",
"reply": "Pro is $49 a month."
},
{
"id": "b",
"reply": "Pro is $59 a month."
}
]
}Response from the checker
{
"object": "check.batch",
"version": "2026-11-01",
"recordTrust": "caller_supplied_unverified",
"pass": false,
"results": [
{
"id": "a",
"recordTrust": "caller_supplied_unverified",
"pass": true,
"findings": [],
"proof": {
"scope": "operation_and_subject_only",
"factsAuthenticated": false,
"replayProtection": "within_request_only",
"authorizationGranted": false
}
},
{
"id": "b",
"recordTrust": "caller_supplied_unverified",
"pass": false,
"findings": [
{
"rule": "price:mismatch",
"span": [
7,
10
],
"text": "$59",
"why": "Pro is $49 or $490, not $59."
}
],
"proof": {
"scope": "operation_and_subject_only",
"factsAuthenticated": false,
"replayProtection": "within_request_only",
"authorizationGranted": false
}
}
]
}Response reference.
| Field | Type | Meaning |
|---|---|---|
result.recordTrust | Trust | Caller-supplied records are not authenticated by a rule check. |
result.pass | boolean | True when no configured rule produced a finding. |
result.findings | array | Configured rule findings with exact reply spans. |
result.proof | Proof | Scope and authority limits of this check. |
result.receipts | Receipts | Optional receipt verification results, when supplied. |
finding.rule | string | The configured rule that produced this finding. |
finding.span | array | UTF-16 offsets into the original reply, [start, end). |
finding.text | string | reply.slice(span[0], span[1]). Display as text, never as markup. |
finding.why | string | Explanation of the violated rule or missing record. |
proof.scope | const | Checks operation and subject binding only. |
proof.factsAuthenticated | const | False: caller facts are not authenticated. |
proof.replayProtection | const | Duplicate handling is limited to this request. |
proof.authorizationGranted | const | False: a check never grants execution permission. |
Generated from the OpenAPI 3.1 contract, which the contract test suite holds equal to the runtime.
Errors you can rely on.
Nothing is silently green: a typo, an unknown version or a malformed body is an error, never a pass. These are the handler’s actual statuses and messages.
| Case | HTTP | Type | Message |
|---|---|---|---|
| Misspelled field | 400 | invalid_request | policy.forbids: unknown field (allowed: name, description, prices, forbid, actions, discounts, unknownAmounts) |
| Unsupported version | 400 | invalid_request | version: unsupported "2099-01-01" (supported: 2026-10-27, 2026-11-01) |
| Both reply and cases | 400 | invalid_request | send reply or cases, not both |
| Malformed JSON | 400 | invalid_request | Body is not valid JSON |
| Wrong content type | 415 | invalid_request | Content-Type must be application/json |
| Body over 64 KiB | 413 | invalid_request | Body exceeds 65536 bytes |
Limits: 64 KiB body · 4,000 characters per reply · 50 replies per batch · burst limit returns 429 with Retry-After · responses are no-store.
The API surface.
| Endpoint | Purpose | Access |
|---|---|---|
| POST https://api.curbi.io/v1/check | Single or batch reply checks | public access |
| GET https://api.curbi.io/v1/check | Version, limits, contract links | Public |
| POST https://api.curbi.io/mcp | Read-only MCP tools | public access |
| GET https://curbi.io/api/platform | Public platform catalog | Read-only |
| GET https://api.curbi.io/.well-known/curbi-receipt-keys.json | Receipt verification keys | Public keys and validity windows |
| GET, POST https://curbi.io/api/v1/projects | Your private policies | Verified developer session |
YOUR NEXT ACTION
Catch one reply. Keep the failure.
Copy this command into a terminal. No signup or key. It sends a synthetic reply and policy to Curbi and returns three findings: action:unrecorded, price:mismatch and forbid:guarantee.
curl -fsS https://api.curbi.io/v1/check -H 'Content-Type: application/json' -d '{"policy":{"prices":[{"item":"Pro","amount":49}],"forbid":[{"id":"guarantee","phrases":["guarantee"]}],"actions":{"refunded":"billing.refund"}},"reply":"Done! I'\''ve refunded you, Pro is just $29, and we guarantee it."}'What next: edit the policy in your sandbox, then export a runnable regression from the MCP console. Connect your framework → · All commands as JSON →
A pass means no configured rule fired. It grants no execution authority or provider-completion proof. public access; production signing is off.
Usage you can inspect. Limits you can lower.
Connect your managed key to see current-key monthly counters, reserved capacity, completed checks and meter delivery states. Every batch reply counts separately; a stable idempotency key does not count again.
Ceilings apply atomically per key and shared billing customer. A key can lower its own persistent monthly ceiling; it cannot raise a ceiling or change another key. Setting zero stops new reservations. Completed usage and existing reservations remain.
GET https://api.curbi.io/v1/usage
Authorization: Bearer <key>
POST https://api.curbi.io/v1/budget
Authorization: Bearer <key>
Content-Type: application/json
{"monthlyBudget":1000}USD $0.001 per completed reply, before tax and invoice adjustments. Meter acceptance is asynchronous and is not a finalized invoice. Billing holds stop new checks while unconfirmed usage is reconciled.
Connect usage and set a lower ceiling →