API reference

Everything the service accepts and everything it can answer. The limits below are fetched from GET https://api.nums.world/v1/limits when this page is served, so they are the ones the service is enforcing right now.

Base URL and authentication

https://api.nums.world

Send the key either way:

Authorization: Bearer csl_...
X-Causilo-Key: csl_...

Routes

MethodPathWhat it doesKey
POST/v1/predictPredict. Multipart with context, query and params, or one Parquet file.yes
GET/v1/limitsWhat this deployment accepts. Numbers a client can check before sending.no
GET/v1/estimateWhether a table of a given shape will be accepted, and how long it will take.no
GET/v1/usageWhat this credential has spent this month and today.yes
GET/v1/contextsThe contexts stored for this caller.yes
DELETE/v1/contexts/{id}Release one stored context.yes
GET/v1/pingWhether the service can take a request.no

There is no cancel. A request you abandon runs to the end and is billed in full — Limits explains why, and POST /v1/cancel answers 501 rather than 404 so that a client calling it is told the feature is missing instead of hunting for a typo.

Sending a prediction

The Python client builds all of this for you; the quickstart is one call in each direction, classification and regression. What follows is the wire format, for callers not using the client.

POST /v1/predict takes multipart with three parts: context and query as Parquet (application/vnd.apache.parquet), and params as JSON. A single Parquet file also works, carrying the same JSON in its causilo-params schema metadata; the rows whose target is missing are the query.

Feature columns must appear in both tables with the same names in the same order. The model reads them by position.

What a column may hold

The first question anybody asks, answered by the code that refuses. Everything below is enforced, and every refusal is a 422 whose detail names the column.

In your tableWhat happens
Missing values in a feature Fine, send them as they are. The model handles them natively. Imputing first is work you do not need to do and information you throw away.
Strings Fine. They are treated as categories. No encoding on your side.
Columns on wildly different scales Fine. No scaling or normalising needed.
A query column that is empty on every row Refused. Left alone the prediction would ignore that column silently.
Dates, times, durations Refused. Split them into numeric parts — year, month, day of week — or send epoch seconds. Preprocessing would treat a date as a category rather than a number, and the answer would be worse without saying so.
Decimal columns Refused. This is what a Postgres or BigQuery NUMERIC becomes in pandas; cast to float first.
Lists, dicts, structsRefused.
Infinities, or values at or above 2.4e+151 Refused. Rescale the column or drop those rows.
float16Widened to float32 for you.
Duplicate column namesRefused.
Your DataFrame indexDropped, never sent. Put anything you need to keep in a column.

Feature columns must match in name and order between the two tables, and the same column must hold the same kind of value in both. The model reads them by position, so a reordered query is refused rather than answered with confident nonsense.

The context must have something that varies. A context whose every feature column holds one value is refused: there is nothing to predict from.

What the target column may hold

The target is in the context and absent from the query. Empty rows in a single Parquet file are the rows to predict.

RuleApplies to
No missing values in the context's targetboth models
At most 64 distinct values, and at least two causilo-clf
Whole numbers or strings, not fractions causilo-clf
Numeric, finite, under 3.4e38causilo-reg
Not a datetimeboth — predictions would come back as epoch integers with no unit

params

context_id is the one worth reading about before you use it: storing a context saves the upload and not the cells, measured both ways in Asking one table many questions.

FieldTypeRequiredMeaning
modelstringyescausilo-clf or causilo-reg.
targetstringwith a context tableThe column holding the answers. Not read when context_id is given: the stored context already knows it.
output_typestringnoPer task, see below. Defaults to the first for the task.
quantileslist of numberswith output_type=quantilesBetween 0 and 1.
cache_contextbooleannoStore this request's context table and return a handle.
context_idstringnoUse a context stored earlier. Send no context part with it.

Unlisted fields are ignored, so a caller moving from another API does not have to strip its knobs out.

output_type

TaskAllowed
classificationprobas, preds (default probas)
regressionmean, median, quantiles (default mean)

What comes back

prediction is one value per query row. metadata carries the shape of the request, the elapsed time, quota, and served_by — the image, the checkpoint and the ensemble width that produced the numbers. Compare served_by before comparing predictions across runs.

Limits

Two different things are called limits here, and they come from different places, so they are two tables.

What your account is allowed

The free tier's allowance. It belongs to your account rather than to the service, so GET /v1/limits does not carry it — what you have left is at GET https://api.nums.world/v1/usage, which needs your key, and every successful response carries the same figures.

Cells per month25,000,000
Cells per day6,250,000
Requests per minute60
Requests per hour1500

What one request may be

GET /v1/limits returns these without a key, so a client can check a table locally before uploading it. Usage, limits and cancellation does exactly that, and handles both rate limits.

Rows300,000
Columns4,096
Cells in one request60,000,000
Classes64
Request bytes1,073,741,824
Response bytes33,554,432
Seconds of GPU per request300

Two ceilings, and which one you meet depends on how wide your table is. max_rows is a flat cap; the deadline refuses a table the service predicts will take longer than 300 seconds, before any GPU time is spent. On the production hardware a narrow table is stopped by the cap — 300,000 rows, context and query together — and a wide one by the deadline, at about 59,939 context rows by a thousand columns. The two swap over somewhere near two hundred columns. The refusal is request_too_large either way, and its detail names a row count that fits at your column count — GET https://api.nums.world/v1/estimate answers the same question before you send anything.

Asking about your own table

GET https://api.nums.world/v1/estimate answers the question the table above cannot: whether your shape will be accepted. It needs no key, uploads nothing, and runs the same checks admission control runs, so its verdict is the one a real request would get.

$ curl 'https://api.nums.world/v1/estimate?n_context=300000&n_query=1000&n_features=10'
{
  "accepted": false,
  "cells": 3010000,
  "counts_against_allowance": 3010000,
  "estimated_seconds": 347.88,
  "model": "causilo-clf",
  "deadline_s": 300.0,
  "max_context_rows": 277583,
  "code": "request_too_large",
  "reason": "This table is predicted to take about 348 s of GPU time, over
             this deployment's 300 s limit ..."
}

Everything in the answer is a prediction from a fitted model except the cell count, which is exact. max_context_rows is how many context rows fit at your column count — the number a refusal would have named, given before the upload rather than after it. When a shape is refused, reason says why, in the words the refusal would use.

Nothing is invoiced. These are an allowance: when it runs out the service answers 429 until it resets, and there is no bill and no overage.

Sending more than one call at a time

The rate limits above are not a parallelism budget. Calls beyond what the service is running are queued and answered in turn — none is refused for being concurrent, and the wait is part of your latency rather than an error. Two things follow, and both hold whatever capacity we are running.

How much fanning out buys you depends on the table. A call spends time on things that are not the GPU — the table goes to storage, the request queues, the answer comes back — and those overlap between calls while the GPU work does not. So the smaller the table, the more a worker pool helps. Measured on 2026-09-18, four calls against one instance:

Context rowsFour in sequenceFour at once
4,000 × 123.03 s1.78 s1.7× faster
20,000 × 129.24 s8.13 s1.1× faster

At 20,000 rows the card is the bottleneck and the calls serialise on it whatever you do. Two or three workers are worth having for small tables; a large pool is not.

Either way, if you have many small queries against one table, send them as one call with more query rows rather than as many calls. Query rows cost almost nothing next to context rows, so one call of a thousand queries is far faster than a thousand calls of one — and unlike fanning out, that holds at every table size.

A large table delays what is behind it. A call that runs for four minutes is four minutes the queue is not moving. Where latency matters, keep tables small and store the context rather than resending it.

elapsed_s in the response is the prediction alone and excludes any time the call spent queued, so a client measuring the wall clock will see a larger number. Both are right; the difference is the queue.

A cell is one value in a table you send, counting context and query rows together and excluding the target column. A request naming a stored context counts the context rows too — storing saves the upload, not the cells.

A request that fails does not count against the allowance, and neither does one that produces a response too large to return.

A request you abandon does count, in full. Hanging up does not reach the machine running the prediction — it finishes the work whatever the caller does — and a call that has started cannot be cancelled; POST /v1/cancel answers 501 for that reason. The client reports this as CausiloTimeout and does not retry, because a retry would count again. Give it a timeout long enough that you read the answer you have already spent.

Errors

Errors are RFC 7807 problem documents. Branch on code; the status is in the body as well as on the response. Every one carries a request_id, also sent as the X-Request-Id header, and a retryable boolean that says whether sending the same request again can succeed. A 429 and a 503 also carry Retry-After, always in whole seconds. Successful responses carry X-Request-Id as well, in the header only.

Two statuses mean the work already ran. A 504 with code timeout means the service stopped waiting, not that the work stopped: the prediction runs to the end and counts in full. A 502 with code upstream_unavailable means it ran and answered with something we could not read. Both carry retryable: false, because a resend is charged again and buys nothing — send a smaller table instead. The Python client refuses to retry either one whatever the body says.

The type of every error links back to its row below. The Python client raises one exception class per group; the cookbook lists which is which.

codestatusWhat happenedWhat to do
unauthorized401No key, a wrong key, or one that has been revoked.Check the key. A revoked key stops working within about a minute.
validation_failed422The tables or the params do not fit the contract.The detail names the column or field. Feature columns must match in name and order.
bad_request400The body could not be parsed at all.Check the content type and that both parts are present.
empty_body400The request arrived with no body.Send the tables. A proxy that strips bodies will produce this.
payload_too_large413The body is over this deployment's ceiling.Send fewer rows, or store the context once and name it afterwards.
request_too_large413The table would take longer than the deployment allows.The detail says how many context rows fit at your column count.
context_not_found404The stored context has expired or been released.Send the table again. The Python client does this for you.
quota_exceeded429The monthly or daily cell cap.The body says which cap, what is used and when it resets.
rate_limited429Too many requests in the minute or hour window. The cell caps are separate.Wait for the Retry-After seconds and resend.
response_too_large413The prediction is bigger than can be returned.Ask for fewer query rows. Nothing counts against your allowance.
not_ready503The service is starting or being replaced.Retry. This is not an error on your side.
unavailable503The GPU was busy for the whole wait, the service is short of memory, or it is shutting down.Retry. Nothing counts against your allowance.
upstream_unavailable503A store the service depends on could not be reached.Retry.
inference_failed500The prediction failed inside the service.Nothing counts against your allowance. Quote the request_id if it repeats.
client_gone499The caller disconnected before the answer was returned.Recorded in the logs only. You will not receive this one.
cancel_key_in_use409Two requests sent the same cancel key.Use a fresh key per request.
not_found404No such route.Check the path.
method_not_allowed405Wrong method for that route.Check the method.
not_implemented501The route exists but this deployment cannot do it.POST /v1/cancel is the only one. A call that has started cannot be stopped.
timeout504The service did not answer within its own wait.The work is still running and still counts. Do not retry; send a smaller table.

Overview · Cookbook · api@nums.world