Resolve API and versions
Your apps get answers from a decision with one HTTP request: send the input values, get the outputs of the first matching rule.
Every decision page shows a ready-to-copy request for that decision, with its id, its input keys and its current version filled in.
Secret keys
Requests are authorised with a secret key. Create one in Settings → Secret keys → Create secret key.
- Scope. A key for All projects resolves any decision in the workspace. A key for one project resolves only that project's decisions.
- Shown once. The key starts with
sd_and is shown only when you create it. SmartDecision stores only a hash of it, so copy it somewhere safe. - Revoking. You can revoke the keys you created at any time, even while the workspace is read-only. When an admin removes a member, that member's keys stop working.
Use one key per integration, so a leaked key can be revoked without breaking the others.
Request
POST /api/resolve/{decisionId} decisionId is the decision's number, shown in its sample request. Send the request to your
SmartDecision server, for example http://decisions.internal:8080.
Headers
| Header | Value |
|---|---|
Authorization | Required. The secret key itself, like sd_..., with no Bearer prefix. |
SmartDecision-Version |
Recommended. The version of the decision your code was built for, like 3. See
Versions.
|
Body
Send one form field per input, named by the input's key, as application/x-www-form-urlencoded or
multipart/form-data.
- Send every input of the decision, even inputs that the matching rule ignores. An empty value counts as missing.
-
Send values as text in the input's format:
720,true,2026-10-13. See Types. - Fields that are not inputs of the decision are ignored.
curl -X POST http://decisions.internal:8080/api/resolve/42 \
-H "Authorization: sd_your_secret_key" \
-H "SmartDecision-Version: 1" \
-d "credit_score=720" \
-d "age=34" const res = await fetch("http://decisions.internal:8080/api/resolve/42", {
method: "POST",
headers: {
Authorization: "sd_your_secret_key",
"SmartDecision-Version": "1",
},
body: new URLSearchParams({ credit_score: "720", age: "34" }),
});
const { output, rule } = await res.json(); Browsers can call the API from any origin. Keep secret keys on your servers, though: anyone who can read a key can resolve with it.
Response
When a rule matches, the status is 200 and the body holds its outputs and position:
{
"output": { "decision": "approved" },
"rule": { "position": "1" }
} outputhas one field per output key of the matching rule.-
Every value is a JSON string, numbers and true/false values included. Convert them in your code, for example
"0.15"to a number. rule.positionis the matching rule's position in the table, also as a string.
When no rule matches, the status is still 200, with a message and no outputs:
{ "message": "No rules matched" } Check for output before you read it.
Response headers
| Header | When | Meaning |
|---|---|---|
SmartDecision-Version | Every answer | The version of the decision that answered. |
SmartDecision-Latest-Version | Answers from an older version | A newer version exists, and this is its number. |
Deprecation | Answers from an older version |
When that version was replaced, as @ and a Unix time in seconds, like
@1791100800.
|
SmartDecision-License | While the license is not active | See License header. |
Browsers can read all four headers.
Batch requests
To resolve many input sets against one decision in one request, for example to price a whole catalogue:
POST /api/resolve/{decisionId}/batch
The headers are the same. The body is JSON with an inputs array, 1 to 2000 sets per request and
at most 2 MB. Every value must be a JSON string.
curl -X POST http://decisions.internal:8080/api/resolve/42/batch \
-H "Authorization: sd_your_secret_key" \
-H "SmartDecision-Version: 1" \
-H "Content-Type: application/json" \
-d '{"inputs": [
{"credit_score": "720", "age": "34"},
{"credit_score": "610", "age": "45"},
{"credit_score": "680"}
]}'
The results come back in the same order. Each one looks like a single response body. A set that can't be
resolved gets a message instead, and the rest of the batch still runs:
{
"results": [
{ "output": { "decision": "approved" }, "rule": { "position": "1" } },
{ "output": { "decision": "rejected" }, "rule": { "position": "3" } },
{ "message": "Missing input value age" }
]
} Errors
Error bodies are either a JSON string, like "Invalid Client Secret", or an object with a
message field. Handle both.
| Status | Body | Cause |
|---|---|---|
400 | "Missing Client Secret" | No Authorization header. |
400 | "Invalid Client Secret" |
The key is unknown or revoked, or belongs to a removed member. A Bearer prefix also
causes this.
|
400 | "Invalid decision ID" | The id in the URL is not a number. |
400 | {"message": "Missing input value credit_score"} | An input is missing or empty. |
400 | {"message": "Version 2 is not available. The current version is 4."} | SmartDecision-Version names a version that was retired or replaced. |
400 | {"message": "SmartDecision-Version must be a version number, like 4."} | SmartDecision-Version is not a number. |
400 | "Invalid batch: ..." or "A batch takes 1 to 2000 input sets" | Batch only: the body is not valid JSON in the expected shape, a value is not a string, or there are too many sets. |
402 | {"message": "License required"} | No license key, or the key is not valid. See License. |
402 | {"message": "License expired"} | The license has expired and passed its cutoff (observed day 31, unless your key sets an earlier one). Add a renewed key. |
403 | "Inactive decision" | The decision is inactive, or its AI draft is still pending or failed. |
404 | "Decision not found" | No decision has this id, or the key is scoped to a different project. |
413 | Batch only: the body is larger than 2 MB. | |
429 | {"message": "rate limit exceeded"} | More than about 20 requests a second from one IP address. Slow down and retry, or use a batch request. |
500 | "Failed to compare values" |
A value can't be read as its input's type, like abc for a number or yes
for a true/false input.
|
Versions
Most changes go live for every caller when you save: a new threshold, a different answer, a reordered rule.
Some changes would break the code that calls the decision. Those make a new version instead, and the version
before keeps answering, so nothing breaks the moment someone saves.
What counts as a breaking change
SmartDecision compares the table you save with the current version, field by field, using keys:
Change Breaking Why Input added Yes Callers don't send it yet, so they would get "Missing input value". Input type changed Yes Values are compared differently. Output removed, or its key changed Yes Callers read a field that is gone. Output type changed Yes Callers parse it differently. A text or true/false output can return a value it couldn't before Yes Callers' code has no branch for it. Input removed No A field callers still send is ignored. Output added No Callers ignore fields they don't read. A text or true/false output value no longer returned No Callers stop seeing it. Thresholds, rule order, number output values, answers that use existing values, renamed labels No Callers send and read the same fields.
An input key change counts as one input removed and one added, so it is breaking. The possible values of a
text or true/false output are the values its rules return. True and true count as the
same value.
When a new version is made
-
A decision starts at version 1. The number goes up only on a breaking save, and the save button says
Save as version N before you confirm.
-
Only versions in use get a new one. While the current version has never answered a Resolve API request, a
breaking save changes it in place. Running tests in the app doesn't count as a request.
-
A decision keeps two versions at most: the current one and the previous one. A second breaking change before
callers have moved drops the oldest version. The save asks first, and says how many requests that version
answered in the last 7 days.
- The editor and tests always use the current version.
Which version answers
Request Answered by No SmartDecision-Version header The previous version while there is one, otherwise the current one. Header names the current or the previous version That version. Header names any other version 400, naming the current version.
Response bodies never change between versions. Only the headers tell callers which version answered. Send the
header with every request, and log or alert on SmartDecision-Latest-Version: when it appears, a
newer version exists. Check the decision page for what changed, update your code, and send the new number.
Retiring a version
While a previous version exists, the decision page shows it, what changed since, how many requests it answered
in the last 7 days and when it was last called. When nothing uses it any more, choose Retire version
N. The app warns you if it answered requests in the last 7 days, but lets you go ahead. After that,
requests without the header get the current version, and requests that name the retired version get a
400. Retiring is recorded in the activity log.
License header
While the license is active, Resolve responses carry no license header. Otherwise every Resolve response,
402 refusals included, carries SmartDecision-License, so your monitoring sees an expiry coming even if nobody
opens the app. Response bodies don't change.
Value Meaning grace; resolve-days-left=24 The license has expired. Everything still works. Decisions stop resolving in 24 days. read-only; resolve-days-left=9 The workspace is read-only. Decisions still resolve, for 9 more days. cutoff Decisions no longer resolve. Requests get 402 License expired. none No license key is installed. Requests get 402 License required. invalid The installed key is not valid. Requests get 402 License required.
Alert on this header. A renewed key restores everything at once. See
Expiry stages.