Row data never travels in the JSON body
Base origin and auth
HOLLERITH_BASE_URL.
That origin proxies exactly two path prefixes: /v1/* and /sdk/*. Nothing else on it is
part of the API, and there is no other public prefix.
GET /v1/model-limits and the wheel download take no key. Every other endpoint requires one.
- No
Authorizationheader —missing_api_key, 401. - Unrecognized key —
invalid_api_key, 401. - Revoked key —
revoked_api_key, 401. - A key from another organization’s resource —
org_forbidden, 403.
Everything succeeds with 200
Submitting a job returns 200. Never 201, never 202, including for work that has not started. Progress lives in thestatus field of the returned job, not in the HTTP status. Poll for it.
Endpoints
Every field on the wire is camelCase. Ids are opaque strings; do not parse them.
There is no
GET /v1/forecasts/{id} and no bare GET /v1/evaluations/{id}. Both return 400.
Worked example: upload, submit, poll, read
1. Build and hash the payload
trainingFeatures and rows are row records, one object per row keyed by column name.
trainingTarget is a flat list, row-aligned with trainingFeatures.
The payload keys are documented under Payload shapes below. Compact
separators and mtime=0 are not required; they only make the address reproducible across
runs, which is what lets an identical dataset dedupe.
2. Ask for somewhere to put it
3. PUT the bytes
Authorization header. The URL is already signed, and it expires 1 hour after it is
issued.
Object storage answers 200 with an ETag. You need that ETag only for multipart uploads.
4. Submit the job
trainRowCount or
outputRowCount disagrees with what the uploaded object actually contains, the worker fails
the job with schema_mismatch — after it has been queued, not at submit.
5. Poll the job
status moves through queued → running → succeeded or failed. A failed job carries
the typed envelope in error; see Errors.
engineVersion is stamped twice. The control plane writes its configured value at submit —
often eng_pending — and the worker overwrites it with the real eng_ plus 16 hex
characters on completion. Read it from a terminal job, never from a queued one.
6. Read the result
predictions is row-aligned with the rows you uploaded. The result is retained for
1 hour, then /result starts returning training_ref_expired (404).
Poll /result too early and you get a 404
GET /v1/predictions/{id}/result returns job_not_found (404) for any job that has not
succeeded. Queued, running and failed all give the same 404 as an id that does not exist.
It is not 409 and not 425. This is counterintuitive and it is stable, so branch on the
bare-id poll route for status and call /result only once status reads succeeded.
A purged job returns training_ref_expired (404) instead.
Large results come back as a URL
/result then returns resultUrl and resultContentHash in place of the data arrays.
The four data arrays are empty or null in that case. Do not read them — check for resultUrl
first, on every result read.
To finish the read yourself:
- GET the URL. It is presigned and expires 5 minutes after it was issued. Ask for the result again to get a fresh one.
- Gunzip the body. It is gzipped JSON with the same five data fields.
- Verify the sha256. Hash the gzipped bytes you received and compare to
resultContentHash. A mismatch means a corrupt read, not a bad prediction.
Idempotency
idempotencyKey. Replaying the same key with the same payload
returns the same job rather than creating a second one.
Reusing a key with a different payload returns idempotency_conflict (409). The fingerprint
is the job kind, task, target column, content hash, row and column counts, and engine version.
The key is validated only as a string. No length or character constraint is enforced, so the
shape of it is your convention to pick and keep.
A convention that survives contact with a retry loop:
fit:churn-v3:2026-08-07— one fit per dataset version per day.predict:churn-v3:batch-0912— one predict per batch identifier.eval:wine-v2:9f2c41ab— one evaluation per input hash.
classCount, and what omitting it costs
classCount is an optional admission hint on classification submits. It is the number of
distinct labels in your training target.
- Send it and the 160-class ceiling is checked at submit. You get
dataset_too_large(422) from the POST, before the job is queued. - Omit it and the check defers to the worker, which counts labels while streaming the payload. The job is admitted, queued, and then fails mid-flight with the same code, after you have waited for a GPU.
POST /v1/uploads
Response, one of two shapes:
For a multipart upload, PUT each slice to its URL in order, keep each response’s
ETag, then
POST the manifest to completeUrl with Content-Type: application/xml:
payload_too_large (413), before anything is signed.
POST /v1/fits
The response is
{job, context} — two complete objects, not an id.
creating at submit. Poll GET /v1/fits/{contextId} until status is
ready, then reference context.id as fittedContextId on predicts.
GET /v1/fits/{contextId}
Returns a bare FittedContext, not wrapped in anything. status is one of creating,
ready, failed, expired, deleted.
expiresAt is 7 days from creation. There is no endpoint to delete a context before then.
Any extra path segment under /v1/fits/ returns 400 {"error":"invalid_request"}.
POST /v1/predictions
This endpoint accepts two mutually exclusive bodies. The handler tries the inline shape first
and falls back to the context shape.
Inline — training rows and rows to score in one job:
Context-backed — score against a ready fitted context, uploading only the rows to score:
Both return a
PredictionJob.
A typo in the inline shape does not report itself as a typo. The body fails the inline parser,
falls through to the context parser, fails that too, and you get a bare
400 {"error":"invalid_request"} with no indication of which shape was intended or which
field was wrong.
Check fittedContextId first when you see that 400. Its presence or absence is the only thing
that tells the two shapes apart.
GET /v1/predictions/{jobId}
This is the poll route for every job kind, including evaluations and forecasts.
GET /v1/predictions/{jobId}/result
For a forecast job,
predictions is one object per forecast step: item_id, timestamp,
mean, and one key per quantile level named by its stringified level.
POST /v1/evaluations
Identical to /v1/fits except that the row count field is named rowCount.
There is no
outputRowCount. The worker splits the labeled set itself, and the returned job
has kind: "evaluate" and outputRows: 0.
Poll it at GET /v1/predictions/{jobId}. Read it at GET /v1/evaluations/{jobId}/result.
GET /v1/evaluations/{jobId}/result
GET /v1/evaluations/{jobId} without the /result suffix is a 400, not a poll route.
POST /v1/forecasts
The returned job has
kind: "forecast", task: "regression" and targetColumn: "target".
A non-empty covariateColumns must match the payload’s covariate keys exactly, or the job
fails with schema_mismatch. Sending [] skips that comparison rather than tightening it, so
a payload that does carry covariates goes unchecked.
There is no GET /v1/forecasts/{id}. Poll at GET /v1/predictions/{id} and read at
GET /v1/predictions/{id}/result, like any other job.
outputRowCount must equal the number of future rows in your payload, or the horizon times
the number of distinct item_id values. The worker recomputes it and fails on a mismatch.
Forecasting is Beta, and it never uses a fitted context.
GET /v1/model-limits
No key. Cached for 300 seconds.
maxUploadBytes here is the default. A deployment can be configured with a lower ceiling, and
that override is not reflected in this response.
Full context for each number is in Limits and quotas.
Payload shapes
The uploaded object is gzipped JSON. Which keys it needs depends on the job.trainingFeatures, rows, context and future are all arrays of row records — one JSON
object per row, keyed by column name. trainingTarget is a flat array of labels.
A forecast payload must include all four of its keys. future and predictionLength may be
null, but exactly one of them has to be set, and quantiles must be present.
What REST cannot do that the SDK can
Three parameters are not REST fields at all.quantiles, predictionLength and future ride
inside the uploaded payload, not the submit body.
For a regression predict, that means prediction intervals are requested by adding a
quantiles array to the payload before you hash and upload it. There is no submit-body field
for them, so an integration written from the endpoint tables alone cannot ask for intervals.
The SDK is also the only place these live:
- Result reassembly. Fetching, gunzipping and verifying an over-512 KiB result.
- Multipart orchestration. Part splitting, ETag collection and the completion manifest.
- Poll loops. Including retrying the three
worker_*andcache_*conditions.
Errors on this surface
Typed failures return the full envelope withcode, category, problem, cause, fix,
docUrl, retryable and requestId. Branch on code.
Malformed input does not.
400 {"error":"invalid_json"}. A body that parses but
matches no accepted shape returns 400 {"error":"invalid_request"}.
Neither carries a code, a category or a requestId. So “every error is a typed envelope”
is false, and there is nothing to quote to support when you hit one.
The 26 typed codes, their HTTP statuses and which four are retryable are in
Errors.
Checklist for a direct integration
- Hash the gzipped bytes, not the JSON text, and prefix with
sha256:. - Send
bytesas the exact gzipped length. It decides single versus multipart; understate it and you get a plan that does not fit the payload. - Check
kindon the upload response before assuming a single PUT. - Send
classCounton classification so the ceiling is checked at submit. - Send an
idempotencyKeyon every POST, and never reuse one with a changed payload. - Make counts match the payload exactly, or the worker fails the job after queueing.
- Poll the bare-id route, not
/result, untilstatusissucceeded. - Check
resultUrlon every result read, and verify its hash after gunzipping. - Read results inside 1 hour. After that
/resultreturnstraining_ref_expired.
Next
- Python SDK — everything on this page, already written
- Errors — the 26 codes and the four that are retryable
- Limits and quotas — every ceiling and the error you get at it
- Authentication — creating and rotating keys