# API reference
URL: /llms/api

Every endpoint under /v1, in the order you use them, with curl for each. Machine-readable spec at /v1/openapi.json.

***

title: API reference
description: Every endpoint under /v1, in the order you use them, with curl for each. Machine-readable spec at /v1/openapi.json.
--------------------------------------------------------------------------------------------------------------------------------

Base URL `https://cactuscompute.com/v1`. Every response is JSON. The OpenAPI document is at [`/v1/openapi.json`](https://cactuscompute.com/v1/openapi.json).

## Authentication

Create a key in the console under [API Keys](https://cactuscompute.com/dashboard/api-keys) and send it as `Authorization: Bearer needle_ft_…`. A key reads and writes its owner's files, jobs and models and reads plan and usage. Two things need a website login instead of a key: creating keys, and changing billing. If you are an agent, ask your human to do those two and to put the key in the `NEEDLE_API_KEY` environment variable of the shell you run in. Never print the key.

```sh
export NEEDLE_API_KEY=needle_ft_...
API=https://cactuscompute.com/v1
auth="Authorization: Bearer $NEEDLE_API_KEY"
```

## Errors

```json
{"error":{"message":"…","type":"platform_error","code":"quota_exceeded","param":"max_depth","url":"https://cactuscompute.com/dashboard/usage"}}
```

`code` is stable and is what to branch on. `param` names the request field at fault. `url`, when present, is the console page that clears the error: hand it to your human.

| Status | Code                    | Meaning, and what to do                                                                                        |
| ------ | ----------------------- | -------------------------------------------------------------------------------------------------------------- |
| 401    | `unauthorized`          | Missing, revoked or expired key. Ask your human for a new one at the `url`.                                    |
| 403    | `session_required`      | This call needs a website login, not a key.                                                                    |
| 402    | `subscription_required` | An active paid plan is required. Your human purchases Starter or subscribes to Pro at the `url`.               |
| 402    | `quota_exceeded`        | The active plan’s allowance is used up. Starter does not reset: upgrade to Pro. Pro resets at its next period. |
| 409    | `insufficient_storage`  | Delete files or models you no longer need.                                                                     |
| 409    | `job_active`            | One job runs at a time. Poll the running one.                                                                  |
| 409    | `file_in_use`           | The file belongs to a running job. Wait for it.                                                                |
| 400    | `invalid_request`       | Read `param` and `message`, fix the field.                                                                     |
| 429    | `rate_limited`          | Wait the seconds in the `Retry-After` header.                                                                  |
| 429    | `queue_limit_reached`   | Ten jobs already waiting. Wait for one to start or cancel one.                                                 |

A job can also fail after it was accepted. Then `status` is `failed` and `error.code` is one of `invalid_input` (a line in your data is not valid chat-format JSON), `execution_failed`, `deadline_exceeded`, `subscription_required`, `insufficient_storage` or `output_incomplete`.

## 1. Plans and your usage

`GET /plans` needs no key. It returns `price_usd`, `billing_type` (`free`, `one_time`, `monthly`, or `custom`), `duration_days`, `once_per_account`, and limits. Free is a local workflow, not a hosted entitlement; follow its `get_started_url`. Starter uses API plan ID `starter`; Pro uses API plan ID `standard`. Each entry includes `execution_mode` and `features`. The legacy `price_usd_per_month` is null for Starter. Read [pricing and included value](/llms/pricing) before recommending a purchase; the human pays at [Billing](https://cactuscompute.com/dashboard/billing). `GET /billing` returns your plan, its limits and what this period has used. Check it before you submit anything that spends allowance. `starter.eligible` reports purchase eligibility. For Starter, `period_end` is the expiry, not a renewal; `starter.paid_at`, `expires_at`, and `upgraded_at` describe its lifecycle.

```sh
curl $API/plans
curl -H "$auth" $API/billing
```

```json
{"plan":"standard","limits":{"jobs":10,"examples":10000,"storage_bytes":10000000000},"usage":{"jobs":2,"examples":1000,"storage_bytes":48213},"period_end":1761955200}
```

## 2. Draft tools (optional)

`POST /tool_schemas` with a product description of 10 to 1,000 characters returns three to five OpenAI-form tool definitions. Fifteen drafts per day. Read them over and edit before you use them.

```sh
curl -H "$auth" -H "Content-Type: application/json" $API/tool_schemas \
  -d '{"description":"Voice assistant for a smart home app. Users speak short commands to control lights, thermostat and door locks."}'
```

## 3. Upload files (if you have your own)

Three calls: reserve, PUT the bytes, complete. `bytes` must be the exact size.

```sh
size=$(wc -c < train.jsonl)
reservation=$(curl -s -H "$auth" -H "Content-Type: application/json" $API/files \
  -d "{\"name\":\"train.jsonl\",\"bytes\":$size}")
upload_id=$(echo "$reservation" | jq -r .id)
curl -X PUT -H "Content-Type: application/octet-stream" --data-binary @train.jsonl "$(echo "$reservation" | jq -r .url)"
curl -X POST -H "$auth" $API/files/$upload_id/complete
```

The last call returns the file record; its `id` starts with `file-`. Repeat for the validation and test files. Files stay until you delete them (30 days after a plan ends), and `GET /files` lists them.

## 4. Generate files (if you do not)

`POST /generations` with your tools, a count of 100 to 10,000 examples, and ideally a product description and a few example messages. The count is spent at submission.

```sh
curl -H "$auth" -H "Content-Type: application/json" $API/generations -d @- <<'EOF'
{"tools":[{"type":"function","function":{"name":"set_lights","parameters":{"type":"object","properties":{"room":{"type":"string"},"state":{"type":"string","enum":["on","off"]}},"required":["room","state"]}}}],
 "examples":1000,
 "description":"Voice assistant for a smart home app; users speak short commands to control devices.",
 "messages":["turn off the kitchen lights","is the front door locked?"],
 "suffix":"smart-home"}
EOF
```

Poll `GET /generations/{id}` every few seconds until `status` is `succeeded`. The response then lists the three files it published, each with a `group` of `train`, `validation` or `test`:

```json
{"id":"ftjob-…","object":"generation","status":"succeeded","files":[
  {"id":"file-…","filename":"smart-home-training.jsonl","group":"train","ordinal":0},
  {"id":"file-…","filename":"smart-home-validation.jsonl","group":"validation","ordinal":0},
  {"id":"file-…","filename":"smart-home-test.jsonl","group":"test","ordinal":0}]}
```

Download any of them with `GET /files/{id}/content`, which answers 307 to a link that lasts 60 seconds.

## 5. Fine-tune

`POST /fine_tuning/jobs` spends one run. `max_depth` is the largest size to train, from 2 up to the base model's depth; every smaller size is trained and scored too. Read the base depth from `GET /models/needle-3` rather than assuming it.

```sh
curl -H "$auth" -H "Content-Type: application/json" $API/fine_tuning/jobs -d @- <<'EOF'
{"model":"needle-3",
 "training_files":["file-TRAIN"],
 "validation_files":["file-VALIDATION"],
 "test_files":["file-TEST"],
 "max_depth":20,
 "suffix":"smart-home-v1"}
EOF
```

Poll `GET /fine_tuning/jobs/{id}`. `status` goes `queued`, `running`, then `succeeded`, `failed` or `cancelled`. While running, `completed_steps` counts to 5. On success, `evaluations` holds one entry per depth with validation and test scores, and `fine_tuned_model` is the id of the full-size model:

```json
{"status":"succeeded","max_depth":8,"fine_tuned_model":"model-…",
 "evaluations":[{"depth":8,"validation":{"correct":94,"total":100,"excluded":0},"test":{"correct":91,"total":100,"excluded":0}},
                {"depth":7,"validation":{"correct":93,"total":100,"excluded":0},"test":{"correct":90,"total":100,"excluded":0}}]}
```

Cancel with `POST /fine_tuning/jobs/{id}/cancel`. A run that fails returns its allowance; a cancelled one does not.

## 6. Download

`GET /models/{fine_tuned_model}` lists every size as `variants`. `GET /models/{variant_id}/content` answers 307 to a download link that lasts 60 seconds, so follow the redirect straight away:

```sh
curl -L -H "$auth" -o smart-home-8L.cact $API/models/model-…/content
```

The base model and its smaller sizes are there too: `GET /models/needle-3` and `GET /models/needle-3-depth-8/content`. Then run it locally: [https://cactuscompute.com/llms/run-locally](https://cactuscompute.com/llms/run-locally).

## Data format

Each line of a `.jsonl` file is one example: a JSON object with `messages` and `tools`, the same shape as an OpenAI chat request. Tool calls sit on the assistant turn as `tool_calls`, with `arguments` as a JSON string. An example where the right answer is to make no call has an assistant turn with plain text and no `tool_calls`. Those count: staying silent where your data stays silent is a correct answer, and a model trained without them learns to call a tool for every message.

```json
{"messages":[{"role":"user","content":"turn the kitchen lights off"},{"role":"assistant","content":"","tool_calls":[{"id":"call_1","type":"function","function":{"name":"set_lights","arguments":"{\"room\":\"kitchen\",\"state\":\"off\"}"}}]}],"tools":[{"type":"function","function":{"name":"set_lights","description":"Turn the lights in one room on or off.","parameters":{"type":"object","properties":{"room":{"type":"string"},"state":{"type":"string","enum":["on","off"]}},"required":["room","state"]}}}]}
```

Name tools and parameters in snake\_case, in the schemas and in the examples. Needle rewrites names to snake\_case before the model sees them, so a model trained on camelCase learns names it is never shown at inference.

A fine-tune needs three files: training, validation and test. Aim for at least 1,000 training examples and 100 each for validation and test. Less still runs, but the scores get noisy. Nothing is cleaned, deduplicated or repaired: lines reach the trainer as sent, and a file that is not valid chat-format JSON fails the job.

## Scoring

An example is correct when every assistant turn makes exactly the calls the example expects, with the same arguments. Key order and spacing do not matter. Examples with no tools defined are excluded and counted separately. Scores use greedy inference with up to 512 response tokens per turn, which the local runtime reproduces.

## Limits

Use `GET /plans` for current prices and limits; [pricing](/llms/pricing) explains the one-time Starter package and monthly Pro subscription. Both use the same job pipeline and storage accounting. A fine-tune counts as one run at submission and a generation counts its requested examples at submission; a job that fails returns its allowance, a cancelled one does not. Request limits, per account across every key and browser session: 120 reads, 30 writes, 6 uploads, 6 job submissions and 10 cancellations per minute, 15 tool drafts per day, one running job at a time, up to 10 waiting per kind.

## Everything else

* `GET /files`, `GET /generations`, `GET /fine_tuning/jobs`, `GET /models` list with `limit` (1 to 100) and `after` (the last id of the previous page); files and models also take `search`.
* `DELETE` on a file, a model, or a finished job removes it and frees storage.
* `GET /api_keys` lists your keys without secrets; `DELETE /api_keys/{id}` revokes one, including the one you are using.
* `POST /billing/checkout`, `/billing/plan` and `/billing/portal` need a website login and answer 403 to a key.
