Skip to content

Submit and poll

A submit is a POST to the model's entity path with the problem's input and, optionally, how to solve it. It answers 202 with the dataset's metadata at once and solving happens on a worker. Polling the metadata until the status is final is the simplest way to wait, and the right one when nothing else fits; Receiving results is when to prefer a webhook or the event stream.

The request

curl -s -X POST -H "X-API-KEY: $KEY" -H 'Content-Type: application/json' \
  "$API/schedules?name=week-40&tags=site:leeds&tags=weekly&priority=7&configurationId=fast" \
  -d '{
    "modelInput": { "employees": [ … ], "shifts": [ … ] },
    "config": {
      "run": {
        "maxThreadCount": 1,
        "termination": { "spentLimit": "PT5M", "unimprovedSpentLimit": "PT30S" }
      },
      "model": { "overrides": { "undesiredDayForEmployeeWeight": 5, "desiredDayForEmployeeWeight": 0 } }
    }
  }'

modelInput is the model's input, checked against its schema before anything is created. Everything in config is optional, and so is config itself.

Query parameters

Parameter Values Meaning
name up to 255 characters a name for the dataset; also accepted in config.run.name
tags repeatable, up to 100 unique labels to list and filter by, and to route webhooks; also config.run.tags
operation SOLVE (default) or NONE NONE validates and scores without solving; POST /{id} solves later
configurationId a profile's id or name, or standard the profile whose configuration applies under the request's own
priority 0 to 10, default 5 ordering within the tenant's queue; higher first

Termination

Field Type Meaning
spentLimit ISO-8601 duration the most time to spend
unimprovedSpentLimit ISO-8601 duration stop when no improvement has been found for this long
slidingWindowDuration and minimumImprovementRatio duration and ratio stop when the score improved by less than the ratio over the window: diminishing returns
stepCountLimit integer stop after this many steps; independent of hardware
moveCountLimit integer stop after this many moves; independent of hardware

With nothing set, the solve stops on diminishing returns with a thirty second window and a ratio of 0.0001. The tenant's lifetime ceiling, twelve hours by default, applies above whatever is asked.

Weights

config.model.overrides has one integer field per constraint, named <key>Weight and listed in the model's descriptor. A missing field means the model's default, and 0 disables the constraint. The fields are typed in the model's schema, so an unknown field or a negative value is a 400.

Threads

maxThreadCount is validated and capped at 1: the Community edition of Timefold Solver solves single-threaded. A higher value is not an error, it is reduced.

How configuration is resolved

Highest first: the request's own config; the profile named by configurationId; for a derived dataset, the parent's configuration; the model's defaults; and last the caps the installation enforces. GET /{id}/config returns the result of that resolution, which is what the worker used.

Idempotency and compression

A retried submit should not solve twice. Send an Idempotency-Key header, and a repeat with the same key within 24 hours returns the existing dataset's metadata instead of creating another.

A body may be sent with Content-Encoding: gzip. The limits are 100 MB compressed and 2 GB inflated; over either is a 413.

What can go wrong

Status Code Why
400 validation the input violates the model's schema, or the configuration is invalid; details names each problem
400 bad-request an unknown operation, a priority outside 0 to 10, an unknown profile
401 no key, or a key the service does not know
403 forbidden a read-only key
413 payload-too-large over the compressed or inflated limit
429 rate-limited over the tenant's submits per minute

Poll the metadata

curl -s -H "X-API-KEY: $KEY" $API/schedules/$ID/metadata
{"id":"ds_01K6…","parentId":null,"originId":null,"name":"week-40","tags":["site:leeds","weekly"],
 "solverStatus":"SOLVING_ACTIVE","score":"0hard/-2085soft",
 "submitDateTime":"2026-09-28T10:15:02Z","startDateTime":"2026-09-28T10:15:03Z","activeDateTime":"2026-09-28T10:15:04Z",
 "completeDateTime":null,"shutdownDateTime":null,
 "validationResult":{"summary":"OK","errors":[],"warnings":[]},"failureMessage":null,"supersededBy":null,"seq":8}

Watch solverStatus for a final status: SOLVING_COMPLETED, SOLVING_INCOMPLETE, SOLVING_FAILED, DATASET_INVALID, or DATASET_COMPUTED for operation=NONE. Start with a short interval and lengthen it for a long solve. The five timestamps mark the phase boundaries, so queueing, initialisation and solving time are readable without a metrics call. seq is the dataset's sequence number, which the event stream and webhooks also carry.

Fetch the result

GET /{id} returns the metadata, the best solution so far as modelOutput, the model's inputMetrics and its kpis. It works while the dataset is still solving, returning the best solution found so far. GET /{id}/score-analysis returns the score broken down by constraint, and GET /{id}/validation-result the model's validation issues.

Solve later

A dataset submitted with operation=NONE stops at DATASET_COMPUTED with its input's score and metrics. POST /{id} queues it for solving, with an optional priority. It is a 400 for a dataset that is not at that point.

Terminate

DELETE /{id} stops a solve and returns the dataset with its best solution kept: the status becomes SOLVING_COMPLETED if a solution exists and SOLVING_INCOMPLETE otherwise. Terminating a dataset that is already final changes nothing. Terminate latency is a worker's heartbeat interval, five seconds.

Rename, retag, list

PATCH /{id}/metadata with { "name": …, "tags": […] } changes either. GET /{entity} lists the tenant's datasets newest first, with status and tag filters, both repeatable, and page and size up to 200.