# VANSA API reference

## Connection and request

Base URL: `https://api.vansa.org`. Current model: `vansa-3`.

Send `POST /v1/systemone` with `Authorization: Bearer <VANSA_KEY>`, `Content-Type: application/json`, and a descriptive `User-Agent`. The public edge may reject Python urllib's default user agent with a plain-text `403` response.

| Field | Contract |
| --- | --- |
| `model` | Optional string; use `vansa-3`. Omission selects the deployment's default. |
| `state` | Non-empty string, JSON object, or array containing the context. |
| `questions` | Object containing 1–32 question definitions, keyed by your IDs. IDs must be non-empty strings of at most 64 characters. |
| Question `type` | `choice`, `score`, or `noul`. |
| Question `instructions` | Non-empty string, at most 4,000 characters. |
| Question `criteria` | Type-specific outcomes, as below. |

### A request with all three question types

```json
{
  "model": "vansa-3",
  "state": {
    "service": "Warehouse dispatch",
    "report": "The label printer is offline. All shipments are blocked and the carrier arrives in 30 minutes. A technician is on site."
  },
  "questions": {
    "route": {
      "type": "choice",
      "instructions": "Which team should handle the issue described in `report`? Use unknown if the report lacks enough information.",
      "criteria": {
        "equipment": "A device or physical equipment failure.",
        "software": "An application or integration failure.",
        "operations": "A staffing, scheduling, or process issue.",
        "unknown": "The cause or responsible team is unclear."
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is the issue in `report`?",
      "criteria": [
        "No immediate disruption or deadline.",
        "Work is slowed, but an alternative is available.",
        "Work is blocked or a near-term deadline is at risk."
      ]
    },
    "work_blocked": {
      "type": "noul",
      "instructions": "Does `report` explicitly state that work is blocked?",
      "criteria": {
        "true": "The report explicitly says work cannot continue.",
        "false": "The report does not say work is blocked."
      }
    }
  }
}
```

Choice criteria require 2–128 options. An object maps each name to a description or `null`; JSON values are accepted, but short text descriptions are usually clearer. An array of unique non-empty option names also works.

Score criteria require 2–32 non-empty descriptions, lowest first. Noul criteria are optional; if provided, use an object containing `true` and/or `false` descriptions. These are string keys, not boolean values.

## Typed answers

The response contains `id`, `object: "systemone.result"`, `model`, `created` (Unix seconds), `answers`, `usage`, and `latency_ms`. Each entry in `answers` uses the original question ID.

| Type | Answer fields | Interpretation |
| --- | --- | --- |
| `choice` | `type`, `choice`, `probabilities`, `confidence` | `choice` is one of the supplied option names. `probabilities` maps option names to probabilities. |
| `score` | `type`, `score`, `legend`, `probabilities`, `confidence` | `score = Σ(i × p_i)` on indices `0..N-1`. `legend` maps string indices to level descriptions; `probabilities` uses those same indices. |
| `noul` | `type`, `noul`, `probabilities`, `confidence` | `noul` is `P(true)`, from 0 to 1. `probabilities` contains `false` and `true`. |

For choice and score, `confidence = 1 − H(p) / log(N)`: 0 is uniform and 1 is concentrated on one outcome. For noul, `confidence = max(p, 1 − p)`, from 0.5 to 1. Thus `noul: 0.08` and `confidence: 0.92` means a confident **no**. These confidence definitions are different; do not compare them as a shared scale of correctness.

Use `noul >= 0.5` for a basic yes/no split, or set an application-specific threshold validated on your data. To obtain the most likely discrete score level, take the argmax of `probabilities`. Do not treat an expected score such as `1.6` as a one-based rating or silently round it when the caller needs the most likely level.

`usage.input_tokens` reports prompt tokens. `usage.output_tokens` is `0` because the model does not generate text. `usage.state_tokens` and `usage.truncated` are present when the API tokenizer is available. For Vansa-3, over-limit prompts are rejected rather than truncated; `truncated`, when present, is `false`.

## Server-side JavaScript

Node.js 18 or newer; no SDK required. Pass the `state` and `questions` from the request above. The function makes one request, validates that every question received its expected answer type, and leaves retry scheduling to the caller.

```js
export async function decide(state, questions) {
  const key = process.env.VANSA_KEY;
  if (!key) throw new Error("Set VANSA_KEY in the server environment.");

  const response = await fetch("https://api.vansa.org/v1/systemone", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "User-Agent": "vansa-integration/1.0"
    },
    body: JSON.stringify({ model: "vansa-3", state, questions }),
    signal: AbortSignal.timeout(60_000)
  });

  const text = await response.text();
  let data;
  try { data = JSON.parse(text); } catch { /* Edge errors may not be JSON. */ }

  if (!response.ok) {
    const error = new Error(`VANSA request failed (HTTP ${response.status}).`);
    error.status = response.status;
    error.code = data?.error?.code;
    error.param = data?.error?.param;
    error.requestId = response.headers.get("X-Request-Id");
    error.retryAfter = response.headers.get("Retry-After");
    throw error;
  }
  if (data?.object !== "systemone.result" || !data.answers) {
    throw new Error("VANSA returned an invalid result.");
  }
  for (const [id, question] of Object.entries(questions)) {
    if (data.answers[id]?.type !== question.type) {
      throw new Error("VANSA returned an incomplete or mismatched result.");
    }
  }
  return data;
}
```

Use `result.answers.route.choice`, `result.answers.urgency.score`, and `result.answers.work_blocked.noul` with the request above. Check values against the application's allowed outcomes and ranges before using them to trigger an action. Keep error bodies out of routine logs because they may contain submitted content.

## Limits and failures

Current defaults: 32 questions per request, 128 options per choice, 32 levels per score, 4,000 characters per instruction, and 512 KiB per request body. Vansa-3 supports 16,384 tokens **per question's full prompt**, including state, instructions, criteria, and roughly 100 fixed instruction tokens. A prompt exceeding this limit receives `400`; the state is not silently shortened.

Each question is evaluated separately. More questions and longer context take longer; more than 16 choice options requires multiple passes. Use at least a 60-second client timeout and bounded concurrency. Request quotas are set individually for each API key and may be unlimited. Read them from authenticated `GET /v1/me`; `null` means unlimited. Do not assume a shared per-minute or daily quota. When a daily quota is configured, it resets at 00:00 UTC.

API errors use `{"error":{"type":"...","message":"...","param":"...","code":"..."}}`; `param` and `code` may be absent. API responses normally include `X-Request-Id`. Unexpected errors may include the request ID in their message. Cloudflare errors can be plain text or HTML, with no API request ID.

| Status | Meaning | Handling |
| --- | --- | --- |
| `400` | Invalid request or a prompt over the token limit | Correct the request using `error.param` and the message. Do not retry unchanged. |
| `401` | Missing, invalid, expired, or revoked key | Check credentials without printing them. |
| `404` | Unknown route or model; model errors use `model_not_found` | Correct the URL or model. |
| `413` | Body too large; `request_too_large` | Reduce the request. |
| `429` | `error.code` is `requests_per_minute` or `daily_quota` | Honor `Retry-After`. A daily quota needs its reset time or a changed limit, not a short retry loop. |
| `502` | Model server unreachable or failed | Retry with bounded exponential backoff and jitter. |
| `503` | `model_loading` or `server_busy` | Honor `Retry-After`, with bounded retries. |
| Other `5xx` or transient network error | Service or transport failure | Retry within the application's attempt/time budget; surface failure when exhausted. |

Quota errors include a `Retry-After` header in seconds. If a 429 has an unrecognized or missing `error.code`, do not assume a short-lived limit: honor the header if present and stop when the retry budget is exhausted.

Do not retry authentication or validation failures unchanged. For transient failures, cap retries (for example, three attempts); if `Retry-After` exceeds the current operation's time budget, return or schedule for later. A timed-out request may have completed and consumed quota, so do not assume it was cancelled at the server. Retrying inference must not repeat a downstream action.

Public `GET /v1/models` lists available models; public `GET /health` reports readiness. Use [the current documentation](https://docs.vansa.org/) and [the OpenAPI schema](https://api.vansa.org/openapi.json) to resolve deployment differences. Neither documentation hosts nor a local preview URL replaces the API base URL.
