Skip to content

Completion

POST /v1/completion is the engine's front door. You give it an input and a mode; it enqueues a run and returns a job envelope. The run itself is asynchronous — read the result from /v1/jobs/{id}.

Want to know the price first? Send the same body to POST /v1/completion/estimate — it answers what the run would cost without running anything or charging anything.


POST /v1/completion

Enqueue an engine run. Returns a pending JobStatus with an id of the form job-<uuid>.

Input: prompt or messages

Provide exactly one of these (supplying both or neither is a 422):

Field Type Description
prompt string A single instruction or question. Up to 500,000 characters.
messages array An OpenAI-format chat history — a list of { "role", "content" } turns. The last turn must be role: "user".

For messages, role is one of system, user, or assistant; the summed length of all content is capped at 500,000 characters (1–256 turns).

Body

Field Type Required Default Description
prompt string one of Single-string input (see above).
messages array one of Chat-history input (see above).
mode string Which orchestration to run. See Modes.
main string resolved The lead companion (name or cmp_<uuid>). If omitted, it is resolved from your preferences.
participants string[] conditional The companions to involve. Required for multi-persona modes unless supplied by a preference.
stream boolean false Hint that you intend to subscribe to the event stream.
settings object Per-call overrides. See Settings.
tools array Client-side tools the model may call. See Client tools.

Companions and teams

A companion is a persona; a team groups companions. List the ones you can use with GET /v1/discover, and pass their names or ids as main / participants.

Modes

Mode What it produces
answer One companion's answer. Set settings.crumbs: true to deepen it over internal reasoning iterations.
parallel Every participant answers independently — N responses, no synthesis.
parallel_with_main Participants answer in parallel; a lead companion synthesizes them.
panel A two-stage expert panel that converges on a synthesized answer.
discussion A moderated, multi-turn discussion ending in a summary.

Settings

settings is the single per-call override object. Every field is optional.

Field Type Description
model string Model slug for the answering personas. Must appear in GET /v1/models.
temperature number Sampling temperature, range-checked against the model's bounds.
top_p number Nucleus-sampling cutoff, range-checked against the model's bounds.
crumbs boolean Toggle the reasoning-deepening loop. On mode: "answer", crumbs: true deepens the single answer over internal iterations (the run stays mode: "answer").
zdr boolean Per-call Zero Data Retention. When true, the run uses only non-retaining providers (some models become unavailable) and only vendors vetted for it — web search is dropped while it is on. It does not change how long the run's content is kept. Omit to inherit your account default.
web_search boolean Per-call simple web search — quick lookups, run provider-side. When true, personas can search the web and cite live sources. Omit to inherit your account default.
budget_guard boolean Per-call budget guard. When false, this run skips the pre-flight cost check even if your account default is on. Omit to inherit your account default.

Examples

curl https://api.humx.ai/v1/completion \
  -H "Authorization: ApiKey $COMPANIONS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "prompt": "What are the trade-offs of optimistic locking?",
        "mode": "answer",
        "settings": { "temperature": 0.4 }
      }'
import os, requests

r = requests.post(
    "https://api.humx.ai/v1/completion",
    headers={"Authorization": f"ApiKey {os.environ['COMPANIONS_API_KEY']}"},
    json={
        "prompt": "What are the trade-offs of optimistic locking?",
        "mode": "answer",
        "settings": {"temperature": 0.4},
    },
)
r.raise_for_status()
job = r.json()
print(job["id"], job["status"])   # job-... pending
const res = await fetch("https://api.humx.ai/v1/completion", {
  method: "POST",
  headers: {
    Authorization: `ApiKey ${process.env.COMPANIONS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "What are the trade-offs of optimistic locking?",
    mode: "answer",
    settings: { temperature: 0.4 },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = await res.json();
console.log(job.id, job.status); // job-... pending
interface JobStatus {
  id: string;
  kind: "completion" | "companion" | "team" | "forensics";
  status: "pending" | "running" | "complete" | "failed" | "requires_action";
  content?: unknown;
}

const res = await fetch("https://api.humx.ai/v1/completion", {
  method: "POST",
  headers: {
    Authorization: `ApiKey ${process.env.COMPANIONS_API_KEY!}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    prompt: "What are the trade-offs of optimistic locking?",
    mode: "answer",
    settings: { temperature: 0.4 },
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = (await res.json()) as JobStatus;

A multi-persona example — a parallel run across three companions:

{
  "prompt": "Should we adopt event sourcing for the orders service?",
  "mode": "parallel",
  "participants": ["architect", "sre", "product-lead"]
}

Response

200 — a pending job envelope. The answer arrives later via /v1/jobs/{id}.

{
  "type": "status",
  "id": "job-1a2b3c4d-...",
  "kind": "completion",
  "status": "pending",
  "mode": "answer"
}

Once complete, the same job carries a typed content keyed by shape:

{
  "id": "job-1a2b3c4d-...",
  "kind": "completion",
  "status": "complete",
  "content": {
    "shape": "answer",
    "companion": "Ada",
    "response": "Optimistic locking trades..."
  }
}

See Jobs → Result shapes for every mode's content.


Estimating a run

POST /v1/completion/estimate takes the exact same body as POST /v1/completion and answers what that run would cost instead of running it. Nothing is created and nothing is charged — no job, no balance movement. To budget a call, re-send what you were about to send:

curl https://api.humx.ai/v1/completion/estimate \
  -H "Authorization: ApiKey $COMPANIONS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "prompt": "Should we rewrite the billing service in Rust?",
        "mode": "panel",
        "participants": ["architect", "sre"]
      }'
{
  "mode": "panel",
  "currency": "USD",
  "estimable": true,
  "runs": 1,
  "estimate": { "expected": 0.0431, "low": 0.0189, "high": 0.1204 },
  "tokens": { "input": 38210, "output": 6100 },
  "calls":  { "expected": 14, "high": 14 },
  "confidence": "medium",
  "basis": "mixed",
  "caveats": [],
  "reasons": [],
  "balance": { "current": 2.5, "sufficient": true },
  "stages": [ { "...": "one row per stage, for auditing the total" } ]
}

Read it in this order: estimableconfidence and basisestimate.expected and balance.sufficientcaveats and stages.

The band. expected prices the calls the run must make at each stage's typical output length; low is the same calls at the 10th percentile; high is every call — including optional ones such as a tool loop running to its cap — at the 90th percentile. Budget against expected; size headroom against high. Money has four decimal places, matching your billing ledger.

confidence (high / medium / low) is the worst grade of any input the estimate consumed. One stage with no usable history drags the whole record down — deliberately, so you never have to read stages[] to learn the number is soft.

basis says where the numbers came from:

Value Meaning
personal your own past runs of this stage
companion a public persona's cross-user history
global the cross-user rollup for this stage
catalogue at least one call was priced from the provider price list rather than run history
constant no history anywhere; built-in fallback token counts
mixed more than one of the above contributed

estimable: false is an answer, not an error. A run whose cost cannot be known ahead of time comes back 200 with every number zeroed, balance.sufficient: null, and stable machine-readable tokens in reasons[] — for example run_structure_not_predictable:<mode> (the graph's shape is decided at run time), no_rate_for_model:<slug> (a model with no history and no catalogue price — unknown, not free), or resolution_failed:<code> (the same problem the real POST would refuse with a 4xx, e.g. an ambiguous companion name). Branch on reasons; caveats is the human-readable half and is free to change.

Relationship to the budget guard

The budget guard runs this same pricing before every enqueue and refuses with 402 when expected exceeds your balance. The estimate endpoint itself is never guarded — asking the price is always free — and a non-estimable run always passes the guard: an unknown is not a refusal.


Client tools

Attach a tools array to declare functions the model can call on your machine (available for mode: "answer", with or without settings.crumbs). When the model calls one, the run pauses and hands the call back — the server never executes your tools. You run them and return the results via POST /v1/jobs/{id}/tool_outputs.

{
  "prompt": "What's in my workspace?",
  "mode": "answer",
  "tools": [
    {
      "name": "read_user_file",
      "description": "Read a file from the user's local workspace.",
      "parameters": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Path relative to workspace root" }
        },
        "required": ["path"]
      }
    }
  ]
}

parameters is plain, provider-neutral JSON Schema. Bounds: up to 32 tools; each name matches ^[a-zA-Z_][a-zA-Z0-9_]*$ (1–64 chars, unique per request); parameters ≤ 16 KiB serialized. The full pause/resume loop lives on the Jobs page.

Validation

A 422 is returned before any run is enqueued when, for example:

type Trigger
prompt_or_messages Both or neither of prompt / messages supplied.
last_message_not_user The final messages turn is not role: "user".
messages_too_large Summed content exceeds 500,000 characters.
unknown_model settings.model is not in your GET /v1/models allow-list.
value_error (temperature / top_p) The value is outside the model's published range.

Insufficient balance surfaces as 402 — before anything is enqueued when the budget guard prices the run above your balance (the body then carries estimate, balance, and shortfall), or when a running call is charged.