LLM decisions over HTTP: one call, any tool
Get typed LLM decisions from any language or no-code tool with two HTTP calls: decide and feedback. Request, response, routing order, errors.
An LLM classification REST API lets any language or no-code tool get a typed answer from a model with one HTTP call. With Curva, that call is `POST /v1/decide`: you send the data (the state) and your typed questions as JSON, and you get back one of your labels per question, with a probability for every option. A second call, `POST /v1/feedback`, sends the true answer when you learn it. That is the whole contract. This guide covers the request, the response fields to branch on, the routing order that works in every tool, auth, errors and request ids.
Curva is a decision server you run yourself. It is free to use under the Curva Free License, and you pay only your model provider. Anything that can send an HTTP request can use it: curl, a webhook in your own app, Zapier, Make, Pipedream, Power Automate, Retool or Airtable automations.
The contract in four lines
The docs reduce the API to this:
POST {base}/v1/decide Authorization: Bearer curva_… Content-Type: application/json
{"state": <any JSON>, "questions": {<key>: <question>}, "project": "…"}
→ {"id": "dec_…", "answers": {<key>: {"choice" | "score" | "noul" | "selected", "confidence", "abstain", …}}}
POST {base}/v1/feedback {"decision_id": "dec_…", "question": <key>, "label": <true answer>}You never parse free text. Every answer is mapped onto the labels you declared. The HTTP API is versioned under `/v1`, and v1 is frozen: changes are additive only, and no field is ever removed or renamed. A client you write today keeps working.
The LLM classification REST API request: state, questions, project, model
The smallest useful request, from the getting-started page:
curl -s localhost:7777/v1/decide -H 'content-type: application/json' -d '{
"state": {"ticket": "The app crashes on launch"},
"questions": {"team": {"type": "choice", "instructions": "Which team?",
"options": {"billing": "", "technical": ""}}}
}'The fields you will use most:
A question has a `type` and `instructions`. A `choice` takes `options` (key to description, which may be empty), a `score` takes `levels` (lowest first), and a `noul` is a yes or no question that returns P(yes). There are also `multi`, `text`, `number` and `integer`. Add `min_confidence` to any question that routes work; that is what turns on the abstain flag below.
Response fields to branch on: choice, confidence, abstain, noul
Here is a real response from the HTTP reference, for a ticket with three questions:
{
"id": "dec_19294a3c1f2000000",
"model": "inclusionai/ling-3.0-flash-fin:free",
"mode": "logprobs",
"latency_ms": 1144,
"cost_usd": 0.0,
"cached": false,
"answers": {
"department": { "choice": "billing", "probabilities": { "billing": 0.9999, "technical": 0.0, "sales": 0.0001 }, "confidence": 0.9999 },
"frustration": { "score": 0.65, "probabilities": [0.36, 0.62, 0.02], "confidence": 0.62 },
"refund_requested": { "noul": 0.999 }
}
}What each field gives a branch step:
Also keep `id`. It is how feedback finds the decision later.
Routing order: abstain, then choice, then a yes/no threshold
Every tool has some branch step: Paths in Zapier, a Router in Make, Switch or If elsewhere. Branch in this order:
- 1POST /v1/decide response
- 2abstain is true?
- 3human review
- 4choice
- 5that route
- 6noul above your threshold?
- 7yes branch
- 8no branch
Check abstain first, then the chosen option, then the yes/no probability against your own threshold.
Abstain comes first because a low-confidence answer still has a `choice`. If you branch on `choice` first, unsure answers get routed as if they were sure.
Send the true answer back
When a person decides, or the outcome becomes known, send it:
{ "decision_id": "dec_…", "question": "department", "label": "billing" }The label is the option key for a Choice, the level index for a Score, `true` or `false` for a Noul, or the true value for an extraction question. The response says how many labels the question has and whether it is calibrated. From 30 labels for the same exact question in a project, Curva fits a calibrator and keeps it only when it makes the probabilities more accurate on held-out labels. Sending feedback again for the same decision and question replaces the earlier label.
A human review branch is the natural place for this call. The person's answer becomes a label, so the queue that catches unsure answers also teaches Curva.
Auth: Bearer curva_ keys and the routes that need none
Create a key with `curva keys create --name <who>`. It prints the key once and stores only a SHA-256 hash. Once any key exists, every route needs `Authorization: Bearer curva_…`, except three that hold no data: `GET /health`, the dashboard page and `GET /openapi.json`.
A few details matter for tools:
Errors: {error: {type, message}} and what to retry
Every error has the same shape: `{"error": {"type": "...", "message": "..."}}`.
Retry 429 and 502. Don't retry the 4xx errors without changing the request. The most common cause of a 400 in no-code tools is text with quotes or newlines pasted into a JSON template. Build the body with the tool's JSON helper or a code step instead.
x-request-id across services
Every response carries an `x-request-id` header. Send your own (1 to 64 visible ASCII characters) and the same id appears in the server's log line for the request. Pass your workflow's run id, and you can follow one item from the trigger through Curva's log to the action it caused.
Pick your tool: curl, OpenAPI, Zapier, Make, Pipedream, Power Automate
The contract is the same everywhere. What differs is how each tool builds the body and branches.
None of these tools has a Curva marketplace app yet. Everything uses their generic HTTP features. For n8n there is a dedicated community node.
Next steps
The full contract is in the [HTTP API reference](https://itsmohitrohilla.github.io/curva-docs/reference/http-api/), and the tool patterns are in [Pipedream and any HTTP tool](https://itsmohitrohilla.github.io/curva-docs/guides/any-http-tool/). For n8n, read [n8n AI routing with a Needs review branch](/blog/n8n-ai-routing/). For code, start with the [Python LLM classification tutorial](/blog/python-llm-classification/) or [TypeScript LLM classification](/blog/typescript-llm-classification/). For what Curva is, see [what is Curva](/blog/what-is-curva/). Install the server with `pip install curva-ai` and run `curva serve`.