Skip to content

2BA.AI / Developer docs

API reference

One base URL, one API key, model amber. Choose an endpoint below.

Get started

Service & authentication

One API key from your dashboard authenticates every endpoint. Everything below is current and verifiable.

Base URL · chat completions
https://api.2ba.ai/v1 — OpenAI compatible
Base URL · Anthropic messages
https://api.2ba.ai — Claude Code appends /v1/messages
Default model
amber — text and vision input
Capacity
4,500 requests per rolling 5-hour window
Concurrency
1–2 agents running simultaneously
Price
€20 per month, flat — cancel anytime
Privacy
Zero prompt logging · 100% EU hosted · GDPR native

Setup

Configure your client

The installer pairs your browser, creates your API key and configures the tools it finds. Per-tool steps live in the client guides; the essentials:

Getting started
curl -fsSL https://2ba.ai/install.sh | sh does the whole pass.
VS Code
OpenAI-compatible provider pointed at https://api.2ba.ai/v1, model amber.
Cursor
Settings → Models: override the OpenAI base URL, model name amber.
Claude Code
ANTHROPIC_BASE_URL=https://api.2ba.ai and ANTHROPIC_AUTH_TOKEN set to your key.
OpenCode
2ba provider with npm @ai-sdk/openai in ~/.config/opencode/opencode.json.
Windsurf
Run OpenCode or Aider in the integrated terminal.
Continue.dev
Custom model provider with apiBase: https://api.2ba.ai/v1.
Aider
aider --model openai/amber with OPENAI_API_BASE=https://api.2ba.ai/v1.
Chat API basics
Completions and streaming against the same base URL and key.

Chat API

Chat completions

Point any OpenAI client at the base URL with your 2BA key. amber takes text and images, streams tokens, and speaks the same wire format your tools already expect.

POST /v1/chat/completionsbash
$ curl https://api.2ba.ai/v1/chat/completions \
  -H "Authorization: Bearer your_2ba_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "model": "amber",
        "messages": [{ "role": "user", "content": "Hello!" }] }'
Endpoint
POST /v1/chat/completions — OpenAI compatible. Anthropic clients use https://api.2ba.ai; Claude Code appends /v1/messages.
Model
amber — text and image input via standard content parts.
Streaming
Set "stream": true for server-sent events in the OpenAI format.
Quota
Chat and agents draw on the 4,500-request rolling 5-hour window; decision mode is metered per request on its own cycle.

OpenAI format

Decisions

POST/v1/decisions

Classify text, check a condition, or score against ordered levels. Send shared input and typed questions; receive answers in the same order.

Model amber · Text only · Zero output tokens

Make a request

POST /v1/decisionsbash
$ curl -X POST https://api.2ba.ai/v1/decisions \
  -H "Authorization: Bearer your_2ba_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "amber",
    "input": "Customer was charged twice for the same invoice.",
    "questions": [{
      "type": "choice", "name": "queue",
      "instructions": "Which queue should handle this request?",
      "choices": [
        {"value": "billing"},
        {"value": "technical"},
        {"value": "sales"}
      ]
    }]
  }'
OpenAI SDK · Python 3.26.0+python
from openai import OpenAI

client = OpenAI(base_url="https://api.2ba.ai/v1", api_key="your_2ba_api_key")
d = client.decisions.create(
    model="amber",
    input="Customer was charged twice for the same invoice.",
    questions=[{
        "type": "choice", "name": "queue",
        "instructions": "Which queue should handle this request?",
        "choices": [{"value": "billing"}, {"value": "technical"}, {"value": "sales"}],
    }],
)
answer = d.answers[0]
if answer.type == "choice":
    print(answer.choice, answer.confidence)
else:
    print("Needs review:", answer.name)
Example response 200 OK · JSON

Illustrative values. Each answer corresponds to a question in the request.

{
  "model": "amber",
  "answers": [{
    "type": "choice", "name": "queue", "choice": "billing",
    "confidence": 0.85,
    "probabilities": [
      {"value": "billing", "probability": 0.9},
      {"value": "technical", "probability": 0.05},
      {"value": "sales", "probability": 0.05}
    ]
  }],
  "usage": {
    "input_tokens": 300, "output_tokens": 0, "total_tokens": 300,
    "input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
    "output_tokens_details": {"reasoning_tokens": 0}
  }
}

Choose a question type

Ask 1–400 questions per request. Each needs instructions; an optional name identifies its answer.

Decision question types and returned fields
TypeProvideReceive
predicateA condition to check.probability that it is true, from 0 to 1.
choice2–26 choices with unique string or boolean values and optional descriptions.Your choice, confidence, and probabilities: [{value, probability}].
score2–10 ordered levels, each with a label and optional description.The weighted level index as score, confidence, and probabilities: [{value, label, probability}].

Handle refusals. Any question can return type: "refusal" with its name. Check the type before reading answer fields. Other questions can still receive answers.

Input, limits & usage Reference
input
Nonempty, non-whitespace text string or ordered messages with text content. Blank string input returns 400 before quota admission. Text function/custom tool calls and matching outputs are accepted as evidence. Native scoring renders message/tool arrays as compact JSON.
answers
Answers follow question order and echo the name (null when unnamed). Choices preserve string and boolean values: true and "true" stay distinct. Native label mass below 0.5 yields a refusal. Confidence is not calibrated correctness; validate thresholds on your own data.
usage
input_tokens, output_tokens, total_tokens, input_tokens_details and output_tokens_details. Output and reasoning tokens are zero. Cache token details are zero because native scoring does not expose them.
Model limits
Text only; up to 26 choices and 10 score levels. Images, files, audio, item references and prompt-cache controls return 400 before quota admission. OpenAI supports images and up to 255 choices. safety_identifier is accepted but discarded; it does not enable OpenAI safety policy. See the OpenAI Decisions guide for the wire format.
Accounting
Each question is scored once in one upstream batch. One API call uses one monthly decision request. Rotations, decision_effort, vote split and the legacy response fields apply only to /v1/choices.

Using the existing state/options format? Open the 2BA choices reference. POST /v1/choices keeps that contract.

Choices POST /v1/choices Existing 2BA format

Existing 2BA format

How choices work

POST /v1/choices turns the served model into a typed classifier: your state and 2–26 options go in, one chosen option and per-option probabilities come out — no answer sentence is written. The gateway batches the rotations through native SGLang decision scoring with thinking disabled. Each rotation scores its answer labels directly; no completion tokens are generated.

01 / Letters, not prose

Direct label scoring.

SGLang checks the A–Z answer labels and scores every option at the answer position. There is no generated letter to parse or top-k truncation.

02 / Rotations

Different option orders.

Each decision runs 1–5 cyclic rotations of the option list (default 3) in one upstream batch. Aggregating them reduces sensitivity to option order; more rotations still use more prefill compute.

03 / Abstain

Low label mass abstains.

label_mass is the probability mass on the offered answer labels. Below 0.5 the choice is null and abstained is true — route those to review instead of guessing.

Choices / Request

Request fields

Authentication is the same API key as every other 2BA endpoint. The state can be in any language; it is passed through verbatim. This existing 2BA state/options format remains on POST /v1/choices. For the OpenAI Decisions format, use POST /v1/decisions.

POST /v1/choicesbash
$ curl -X POST https://api.2ba.ai/v1/choices \
  -H "Authorization: Bearer your_2ba_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Customer was charged twice for the same invoice.",
    "question": "Which queue should handle this request?",
    "decision_effort": "high",
    "options": [
      {"id": "billing", "description": "Invoices, payments, refunds"},
      {"id": "technical", "description": "Bugs, outages, system errors"},
      {"id": "sales", "description": "Pricing and account questions"}
    ]
  }'
state
Required, nonempty. The text to classify; any language, passed through verbatim.
question
Optional. Defaults to a generic instruction; use it to steer what the decision is about.
options
2–26 entries with unique nonempty ids. description is optional and falls back to the id.
decision_effort
Optional evaluation compute preset: low = 1 rotation, medium = 2, high = 3. Omit it for the default; explicit null and other invalid values return 400. Thinking stays disabled. These are compute budgets, not guaranteed quality tiers.
rotations
Optional direct count, 1–5. If both controls are supplied, a nonzero count must match the effort preset or the request fails with 400. The default is 3 when neither is supplied; zero is treated as omitted. More rotations use more prefill and can add latency under load.
instructions
Optional extra instruction appended to the decision question.

Choices / Response

Response fields

The response carries the aggregated distribution, the abstain signal, and a per-rotation tally that shows when option orders disagree.

200 OKjson
{
  "model": "amber",
  "choice": "billing",
  "probabilities": {"billing": 0.86, "technical": 0.11, "sales": 0.03},
  "confidence": 0.86,
  "label_mass": 0.97,
  "vote_split": {"billing": 3},
  "abstained": false,
  "rotations": 3,
  "usage": {"prompt_tokens": 651, "completion_tokens": 0}
}
probabilities
Normalized geometric mean of the native per-option probabilities across rotations. Every option is scored directly.
label_mass
Mean full-vocabulary probability mass on the offered answer labels across rotations. Below 0.5 choice is null and abstained is true.
vote_split
Per-rotation argmax tally. Disagreement may warrant review; neither unanimous votes nor high confidence prove correctness. Validate review thresholds on labelled examples from your application.
usage
Prompt tokens summed across rotations, with zero completion tokens. Metered as one request against the monthly decision budget; chat quota is separate.

Choices / Examples

Language examples

Any HTTP client works; the endpoint is plain JSON in, plain JSON out.

decide.pypython
# pip install requests
import requests

API_KEY = "your_2ba_api_key"  # from your 2BA dashboard

d = requests.post("https://api.2ba.ai/v1/choices",
    headers={"Authorization": "Bearer " + API_KEY},
    json={
        "state": "Customer was charged twice for the same invoice.",
        "question": "Which queue should handle this request?",
        "options": [
            {"id": "billing", "description": "Invoices, payments, refunds"},
            {"id": "technical", "description": "Bugs, outages, system errors"},
            {"id": "sales", "description": "Pricing and account questions"}],
    }).json()

print(d["choice"], d["vote_split"], d["abstained"])
main.gogo
// Standard library only.
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
)

func main() {
	body := `{"state": "Customer was charged twice for the same invoice.",
		"question": "Which queue should handle this request?",
		"options": [
			{"id": "billing", "description": "Invoices, payments, refunds"},
			{"id": "technical", "description": "Bugs, outages, system errors"},
			{"id": "sales", "description": "Pricing and account questions"}]}`
	req, err := http.NewRequest("POST", "https://api.2ba.ai/v1/choices", strings.NewReader(body))
	if err != nil {
		panic(err)
	}
	req.Header.Set("Authorization", "Bearer your_2ba_api_key")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var d struct {
		Choice    string         `json:"choice"`
		VoteSplit map[string]int `json:"vote_split"`
		Abstained bool           `json:"abstained"`
	}
	if err := json.NewDecoder(res.Body).Decode(&d); err != nil {
		panic(err)
	}
	fmt.Println(d.Choice, d.VoteSplit, d.Abstained)
}
decide.tstypescript
// Node 18+, Deno and Bun: fetch is built in.
const res = await fetch("https://api.2ba.ai/v1/choices", {
  method: "POST",
  headers: {
    Authorization: "Bearer your_2ba_api_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    state: "Customer was charged twice for the same invoice.",
    question: "Which queue should handle this request?",
    options: [
      { id: "billing", description: "Invoices, payments, refunds" },
      { id: "technical", description: "Bugs, outages, system errors" },
      { id: "sales", description: "Pricing and account questions" },
    ],
  }),
});

const decision = await res.json();
console.log(decision.choice, decision.vote_split, decision.abstained);
Historical evaluation Earlier chat-based implementation

Historical evaluation

Earlier decision implementation

Historical evaluation of the earlier chat-based implementation on a fixed 36-item dataset: one run per configuration, no retries, no prompt tuning per item. Native scoring has not yet been re-evaluated.

Evaluation / 36 itemsCompleted

01 / Product categorization

86% correct.

Items assigned the correct category on the same fixed dataset. Higher is better.

2BA decision mode earlier chat-based path, amber, rotations 3

86%correct

Jev external decision API, as configured by its own docs

81%correct

The earlier path measured ~150 ms and ~4 output tokens per decision. Native scoring now generates zero output tokens. These historical accuracy and latency figures do not establish the native path’s performance.

Internal evaluation, single run per configuration on a 36-item dataset. Not a general-purpose benchmark; validate on your own data before routing production traffic. Coding benchmarks live on the quality page.

Reference

Errors & caveats

Probabilities are peaked.

Label probabilities can be high on correct and incorrect answers alike. Neither confidence nor vote_split is calibrated uncertainty; validate review thresholds on your own data.

No generated answer.

Native scoring reads the exact answer-label probabilities without logit_bias, text generation or output parsing. Scores can still shift with cache state and batch composition.

26 options per question.

The native choice set is capped at 26 (A–Z); /v1/choices accepts one option set per request. For larger sets, compose hierarchically: choose a branch first, then a leaf.

Upstream failures.

The questions or rotations form one native batch. An upstream failure or incomplete scoring response returns 502; no partial decision is returned.

Get started

Create your API key

Pair your browser, create an API key and send your first decision.

Terminal — secure installerCopy