Service consumer guide
This guide is for developers who want to consume a service built with Timefold Solver using the service module.
A Timefold Solver service exposes a REST API with a predictable structure. Every service follows the same lifecycle: submit a problem, wait for the result, and optionally terminate early or inspect the score. The exact endpoint paths and request/response shapes vary per model, but the patterns described here apply to any service built with the service module.
|
If you are integrating with a service deployed on the Timefold Platform, see the platform documentation on receiving model API results for platform-specific callback and webhook mechanisms. |
1. Discover the API
Every Timefold Solver service publishes an OpenAPI specification when run with mvn quarkus:dev.
Use it to discover the exact endpoint paths, request and response schemas, and available operations for the specific model you are integrating with.
You can also explore the specification interactively through the Swagger UI at /q/swagger-ui when the service is running locally.
2. Try it out with demo data
If the service exposes demo data, you can use it to get a realistic request body without constructing one yourself. This is the fastest way to test connectivity with the service.
2.1. List available demo datasets
GET /v<version>/demo-data
Returns a list of available demo dataset identifiers and their descriptions.
[
{
"id": "BASIC",
"shortDescription": "A small dataset suitable for quick tests.",
"longDescription": "10 lessons, 5 timeslots, 2 rooms.",
"tags": ["small"]
},
{
"id": "COMPLEX_SET",
"shortDescription": "A larger, more realistic dataset.",
"longDescription": "200 lessons across multiple rooms and days.",
"tags": ["large", "realistic"]
}
]
2.2. Retrieve a demo dataset
GET /v<version>/demo-data/{demoDataId}
Returns a fully formed solving request body, the same structure expected by POST /<root>.
You can submit it directly or use it as a template to understand the expected input format.
# Retrieve the demo dataset
curl http://localhost:8080/v1/demo-data/BASIC
# Pipe it directly into a solve request
curl -X POST http://localhost:8080/v1/timetables \
-H "Content-Type: application/json" \
-d "$(curl -s http://localhost:8080/v1/demo-data/BASIC)"
3. Submit a run
POST /<root>
Submits a planning problem for optimization and (by default) requests solving immediately.
The service returns a dataset ID right away and processes validation/output computation/solving asynchronously; pass ?operation=NONE to store/validate/compute without solving.
3.1. Request structure
The request body is a JSON envelope with one required top-level field (modelInput) and one optional field (config).
{
"config": {
"run": {
"name": "my-run",
"termination": {
"spentLimit": "PT5M",
"unimprovedSpentLimit": "PT30S"
},
"maxThreadCount": 1,
"tags": ["production", "monday"]
}
},
"modelInput": { (1)
}
}
| 1 | The modelInput field contains your planning problem data.
Its structure is specific to the model — consult the OpenAPI specification for the exact schema. |
The run configuration controls how long the solver runs:
| Field | Description |
|---|---|
|
A human-readable name for the run. Auto-generated if omitted. |
|
Number of solver threads. Defaults to 1. |
|
Optional labels for filtering or grouping runs. |
Termination settings can be specified per dataset to override the hardcoded termination settings.
-
spentLimitsets the maximum duration (in ISO 8601 duration format) for solving a dataset. -
unimprovedSpentLimitsets the maximum duration (in ISO 8601 duration format) for solving a dataset since the dataset score improved. If no value is provided, the default diminished returns termination will apply. If set, stepCountLimit must be empty. Warning: using this option will disable the default diminished returns termination which is recommended for most use cases -
stepCountLimitsets the maximum solver step count for solving a dataset. The solver will stop solving after a pre-determined amount of steps. Use when you require results independently of the hardware resources performance. Use this termination if you want to benchmark your models, not recommended for production use. If set, unimprovedSpentLimit must be empty. Warning: using this option will disable the default diminished returns termination which is recommended for most use cases. -
slidingWindowDurationsets the sliding window (in ISO 8601 duration format) over which score improvement is measured by the diminished returns termination. Defaults to PT30S when omitted. Only takes effect when diminished returns termination is active (i.e. unimprovedSpentLimit and stepCountLimit are both empty). -
minimumImprovementRatiosets the minimum ratio between current and initial improvement before the diminished returns termination kicks in. Must be strictly positive. Defaults to 0.0001 when omitted. Only takes effect when diminished returns termination is active (i.e. unimprovedSpentLimit and stepCountLimit are both empty).
3.2. Response
The service responds with HTTP 202 (Accepted) and a Metadata object.
The key field to capture is id, you will need it to poll for results.
{
"id": "7f3a91bc-4e2d-4c1a-b8f6-1234567890ab", (1)
"name": "my-run",
"solverStatus": "DATASET_CREATED", (2)
"submitDateTime": "2025-06-01T09:00:00Z",
"tags": ["production", "monday"]
}
| 1 | Store this id to poll for results. |
| 2 | The dataset was accepted and will be processed asynchronously (validation, output computation, and solving if requested). |
4. Poll for results
GET /<root>/{id}
Returns the current state of a submitted run, including the best solution found so far.
Unlike the submit response (which returns Metadata only), this endpoint returns the full response envelope (metadata, modelOutput, inputMetrics, kpis).
4.1. Solver status lifecycle
The metadata.solverStatus field follows the dataset lifecycle and may include DATASET_CREATED, DATASET_VALIDATED, DATASET_COMPUTED, or DATASET_INVALID before/around the solving statuses below:
| Status | Meaning |
|---|---|
|
Solving was requested for an existing dataset (for example |
|
The run is initializing: the model is being built and enriched (e.g. loading external data). Solving has not started yet. |
|
The solver is actively optimizing. The |
|
Solving finished normally (termination condition was met). The result is final. |
|
Solving terminated due to an error. |
|
Solving finished without producing a complete solution (for example, terminated before a first score was produced). |
The run is complete once solverStatus is SOLVING_COMPLETED, SOLVING_FAILED, SOLVING_INCOMPLETE, or DATASET_INVALID.
4.2. Polling vs Server-Sent Events
The service supports two ways to track a run in progress. Choose the one that fits your integration style:
| Polling | Server-Sent Events (SSE) | |
|---|---|---|
How it works |
You call |
You open a long-lived |
Response content |
|
Metadata only. Fetch the full result with |
Best for |
Simple integrations, batch backends, or when you only need the final result. |
Live progress UIs, dashboards, or any case where you want immediate notification of status changes. |
Connection |
Stateless — each request is independent. |
Stateful — holds an open HTTP connection until the run completes. |
Terminal state handling |
Detect by checking |
Stop reading after the terminal |
4.3. Polling
The service exposes two endpoints for polling:
-
GET /<root>/{id}— returns the full response envelope includingmodelOutput,inputMetrics, andkpis. Use this when you want to show intermediate results as solving progresses. -
GET /<root>/{id}/metadata— returns only theMetadataobject (status, score, timestamps). Use this when you only care about completion and want to avoid the cost of transferring the full solution on every poll.
A 2–5 second interval is a reasonable default for displaying live progress.
If you only need the final result, poll less frequently or wait until the spentLimit has elapsed before making your first request.
ID="7f3a91bc-4e2d-4c1a-b8f6-1234567890ab"
while true; do
STATUS=$(curl -s http://localhost:8080/v1/timetables/$ID/metadata | jq -r '.solverStatus')
echo "Status: $STATUS"
case $STATUS in
SOLVING_COMPLETED|SOLVING_FAILED|SOLVING_INCOMPLETE|DATASET_INVALID)
echo "Done. Fetching full result..."
curl -s http://localhost:8080/v1/timetables/$ID | jq '.metadata.score'
break
;;
esac
sleep 3
done
4.4. Server-Sent Events
GET /<root>/{id}/events
Opens an SSE stream for a submitted run.
The service sends a Metadata JSON event whenever the run’s metadata is updated (typically on status changes and when a new best solution is found).
The service stops emitting events once the run reaches a terminal state (SOLVING_COMPLETED, SOLVING_FAILED, SOLVING_INCOMPLETE, or DATASET_INVALID); close the connection client-side after receiving the terminal event.
If the run is already in a terminal state when you connect, the service returns HTTP 410 Gone.
In that case, retrieve the final result with a regular GET /<root>/{id} call.
The endpoint accepts an optional, repeatable status query parameter (for example, ?status=SOLVING_COMPLETED&status=SOLVING_FAILED) to receive events only for the specified statuses.
Omit it to receive all transitions.
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/v1/timetables/7f3a91bc-4e2d-4c1a-b8f6-1234567890ab/events"
data: {"id":"7f3a91bc-4e2d-4c1a-b8f6-1234567890ab","solverStatus":"SOLVING_STARTED","score":null,...}
data: {"id":"7f3a91bc-4e2d-4c1a-b8f6-1234567890ab","solverStatus":"SOLVING_ACTIVE","score":"-5hard/-20soft",...}
data: {"id":"7f3a91bc-4e2d-4c1a-b8f6-1234567890ab","solverStatus":"SOLVING_COMPLETED","score":"0hard/-3soft",...}
After receiving the terminal event, fetch the full result (including modelOutput and metrics) with a single GET /<root>/{id} call.
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/v1/timetables/7f3a91bc-4e2d-4c1a-b8f6-1234567890ab/events?status=SOLVING_COMPLETED&status=SOLVING_FAILED"
|
If you are integrating with the Timefold Platform, use the webhook mechanism to receive a callback when solving completes, instead of polling or SSE. |
5. Interpret the response
The response envelope always contains metadata and modelOutput.
Depending on the model, it may also contain inputMetrics and kpis.
5.1. Metadata
{
"metadata": {
"id": "7f3a91bc-4e2d-4c1a-b8f6-1234567890ab",
"name": "my-run",
"solverStatus": "SOLVING_COMPLETED",
"score": "0hard/-3soft",
"submitDateTime": "2025-06-01T09:00:00Z",
"startDateTime": "2025-06-01T09:00:01Z",
"activeDateTime": "2025-06-01T09:00:02Z",
"completeDateTime": "2025-06-01T09:05:00Z",
"shutdownDateTime": "2025-06-01T09:05:01Z",
"tags": ["production", "monday"],
"validationResult": {
"summary": "OK",
"errors": [],
"warnings": []
}
}
}
The score field encodes solution quality.
A score with 0hard means all hard constraints are satisfied and the solution is feasible.
Negative soft scores indicate soft constraint violations; closer to zero is better.
See score documentation for more details.
The timestamps track each phase of the optimization lifecycle:
| Timestamp | Meaning |
|---|---|
|
When the dataset was received. Status: |
|
When model initialization began (loading external data, enrichment). Status: |
|
When the solver began optimizing. Status: |
|
When solving finished. Status: |
|
When post-processing finished (e.g. score analysis, output enrichment). All work is done. |
5.2. Solution output
The modelOutput field contains the solved planning problem in the model’s output format.
Its structure is model-specific — consult the OpenAPI specification.
5.3. Metrics
If the model exposes input and output metrics, they appear as top-level fields:
{
"inputMetrics": {
"lessons": 200,
"timeslots": 50
},
"kpis": {
"unassignedLessons": 0,
"maxConsecutiveLessons": 3
}
}
Use inputMetrics to confirm the solver received the expected problem size,
and kpis to assess solution quality without parsing the full modelOutput.
6. Inspect score analysis
| This feature is exclusive to Timefold Solver Enterprise Edition. |
GET /<root>/{id}/score-analysis
Returns a breakdown of which constraints are satisfied or violated, and by how much. This is useful for debugging infeasible solutions or explaining results to end users.
{
"score": "0hard/-3soft",
"constraints": [
{
"name": "Room conflict",
"score": "0hard",
"matchCount": 0
},
{
"name": "Teacher time preference",
"score": "-3soft",
"matchCount": 3
}
]
}
7. Terminate a run early
DELETE /<root>/{id}
After termination, the solverStatus is final (typically SOLVING_COMPLETED, or SOLVING_INCOMPLETE if no score was produced).
Use this when the user no longer needs a result, or when a better input is available and you want to start a fresh run.
curl -X DELETE http://localhost:8080/v1/timetables/7f3a91bc-4e2d-4c1a-b8f6-1234567890ab
8. Handle validation errors
If the input fails validation, the response includes error details in metadata.validationResult.
{
"metadata": {
"solverStatus": "DATASET_INVALID",
"validationResult": {
"summary": "ERRORS",
"errors": ["Duplicate teacher names found."],
"warnings": []
}
}
}
For structured, machine-readable validation errors, use the dedicated endpoint:
GET /<root>/{id}/validation-result
{
"status": "ERRORS",
"issues": [
{
"id": 1,
"code": "DUPLICATE_TEACHER",
"severity": "ERROR",
"detail": {
"teacherName": "Ann"
}
}
]
}
See Validating REST input for details on how services implement validation.