The NexOR Optimization API
Submit a problem, get a solution. One REST call in, one answer out, stable under /solve/v1 and authenticated with a bearer key. Every endpoint on this page runs today; the Python SDK is a preview and is marked as such.
Free tier, no card. Your first solve takes minutes.
-
POST
/problems -
queuedthe problem has an id -
POST
/problems/{id}/wait -
runningthe wait is held open here -
finishedthe wait returns, or your webhook fires -
GET
/problems/{id}/result -
solutionyou have the answer
A solver you call over HTTP
You send a mathematical optimisation problem as JSON. We run it on our solvers and hand back the solution. It is domain agnostic: linear programs, mixed-integer models, routing, scheduling, whatever you can express, all travel the same envelope.
Every integration has the same three moves: submit a problem and get an id, wait on one request or let a signed webhook reach you, then fetch the solution. The next section does all three in under a minute.
We treat your problem body as opaque. The manager validates the envelope and meters compute, but never reads your model. That is what keeps one API generic across every problem class.
Your first solve, in three steps
From zero to a real answer in minutes. A two-product production mix: maximise margin under machine-hour and material limits. The optimum is 30 chairs and 5 tables, objective 1750.
Test it live first
No account, no key, free. Write a model in Python and run it against the real solver, in your browser.
-
Copy an API key
Give it any name and pick an expiration, that is the whole form. This key is what identifies you on every API call you make. The secret is shown once, so copy it right away. This is the only step that happens in the browser.
Open API keys -
Submit, wait, fetch
Paste your key into the example below and run it: submit the envelope, hold one request open until the problem ends, then fetch the solution. This is the shortest path from a laptop, where nothing can receive a webhook. Once you run a server, register a webhook and drop the middle step.
# Set these once. BASE is your NexOR host; the key comes from your portal.
export BASE="https://<your-nexor-host>/solve/v1"
export API_KEY="<your-api-key>"
# 1. Submit the envelope (saved as envelope.json).
curl -s -X POST "$BASE/problems" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @envelope.json
# -> {"problem_id":"prb_9f3c...","status":"queued","time_limit_seconds":5,...}
# 2. Wait. The request stays open until the problem ends, and timeout_seconds
# is capped at 60, so repeat the call while the status is queued or running.
# A server registers a webhook instead and skips this step altogether.
curl -s -X POST "$BASE/problems/prb_9f3c.../wait?timeout_seconds=60" \
-H "Authorization: Bearer $API_KEY"
# -> the snapshot, with "outcome" set once the solve has ended
# 3. Fetch the solution. Without inline=1 the route answers 302 to a presigned
# object URL, so a plain curl prints nothing; pass -L to follow it instead.
curl -s "$BASE/problems/prb_9f3c.../result?inline=1" \
-H "Authorization: Bearer $API_KEY"
# -> {"problem_id":"prb_9f3c...","outcome":"solved","solution":{
# "outcome":"solved","termination_status":"OPTIMAL","objective_value":1750.0,
# "result":{"values":{"chairs":30.0,"tables":5.0}}}}
import os, time, requests
BASE = os.environ["SOLVE_BASE"] # https://<your-nexor-host>/solve/v1
HEAD = {"Authorization": f"Bearer {os.environ['SOLVE_API_KEY']}"}
envelope = {
"api_version": "1",
"problem": {
"variables": [{"name": "chairs", "lower": 0}, {"name": "tables", "lower": 0}],
"constraints": [
{"expr": "4*chairs + 8*tables <= 160", "name": "machine_hours"},
{"expr": "2*chairs + 3*tables <= 75", "name": "material"},
],
"objective": {"sense": "max", "expr": "45*chairs + 80*tables"},
},
"solver": "highs",
"options": {"compute_preset": "cpu-standard", "time_limit_seconds": 5},
}
# 1. Submit.
prb = requests.post(f"{BASE}/problems", json=envelope, headers=HEAD).json()
print("submitted", prb["problem_id"], prb["status"])
# 2. Wait. The request is held open until the problem ends; the 60 second cap
# means a longer solve answers a snapshot that is not terminal yet, so the
# call is repeated. For live progress open GET /problems/{id}/events instead.
RUNNING = {"queued", "running"}
while True:
reply = requests.post(
f"{BASE}/problems/{prb['problem_id']}/wait",
params={"timeout_seconds": 60},
headers=HEAD,
)
if reply.status_code == 429: # asked to slow down
time.sleep(int(reply.headers.get("Retry-After", 5)))
continue
reply.raise_for_status()
state = reply.json()
if state["status"] not in RUNNING:
break
# 3. Fetch the solution.
reply = requests.get(f"{BASE}/problems/{prb['problem_id']}/result", headers=HEAD)
while reply.status_code == 429: # a burst of submits spent the budget
time.sleep(int(reply.headers.get("Retry-After", 5)))
reply = requests.get(f"{BASE}/problems/{prb['problem_id']}/result", headers=HEAD)
reply.raise_for_status() # 410 once past retention
sol = reply.json()
print("outcome:", sol["outcome"]) # "solved"
print("objective:", sol["solution"]["objective_value"]) # 1750.0
print("values:", sol["solution"]["result"]["values"]) # {"chairs": 30.0, ...}
The envelope.json used above
{
"api_version": "1",
"problem": {
"variables": [
{
"name": "chairs",
"lower": 0
},
{
"name": "tables",
"lower": 0
}
],
"constraints": [
{
"expr": "4*chairs + 8*tables <= 160",
"name": "machine_hours"
},
{
"expr": "2*chairs + 3*tables <= 75",
"name": "material"
}
],
"objective": {
"sense": "max",
"expr": "45*chairs + 80*tables"
}
},
"solver": "highs",
"options": {
"compute_preset": "cpu-standard",
"time_limit_seconds": 5,
"tags": [
"quickstart"
]
}
}
Ready for the details? Full API reference
Bearer keys
Every request carries your API key as a bearer token. Keys are scoped to the optimisation API, so a leaked key cannot touch anything else in your account.
Authorization: Bearer <your-api-key>
Create your first key in the portal. After that you can list, mint and revoke keys over the API itself (see API keys). Keep keys server-side, and rotate them with no downtime when needed.
How a solve flows
Solving is asynchronous. Submit returns immediately with an id and a queued status; the solve runs on our infrastructure; the outcome comes to you.
POST /problems -> id, status: queued
POST /problems/{id}/wait -> held open while the solver works
the solver picks it up -> running
it finishes -> finished (the wait returns)
GET /problems/{id}/result -> the solution itself
Three ways to know it is done. A webhook reaches you the moment the problem ends and costs you no request, which is what a server should use. POST /problems/{id}/wait holds one request open until then, which is what a script on a laptop should use. GET /problems/{id}/events streams the same events over one connection, and carries the solver's progress while it works. Every payload is thin, so you still fetch the solution with one call.
The submit envelope
One versioned wrapper around four parts. We own and validate the wrapper; the problem inside is yours.
- api_version
- The wire version. Always
"1"today. - problem
- Your model: variables, constraints, an objective. Opaque to us, validated by the solver.
- solver
- The engine to run: a concrete solver name, or a meta-solver name, which is a named set of member engines, any of which may take the job. Solver tuning travels inside
problem. - options
time_limit_seconds,compute_preset,webhook,idempotency_keyandtags. All optional.- options.compute_preset
- The compute the solve reserves, by code (
cpu-standard,gpu-standard). Omit it and the engine runs on its own default.GET /solverslists every preset and which engines offer which.
{
"api_version": "1",
"problem": {
"variables": [
{
"name": "chairs",
"lower": 0
},
{
"name": "tables",
"lower": 0
}
],
"constraints": [
{
"expr": "4*chairs + 8*tables <= 160",
"name": "machine_hours"
},
{
"expr": "2*chairs + 3*tables <= 75",
"name": "material"
}
],
"objective": {
"sense": "max",
"expr": "45*chairs + 80*tables"
}
},
"solver": "highs",
"options": {
"compute_preset": "cpu-standard",
"time_limit_seconds": 5,
"tags": [
"quickstart"
]
}
}
Machine-readable JSON Schemas: admission_request_v1.json
The schema types problem as an opaque object, because the manager never reads it. The section below is the shape the solvers do read.
Describing a model
Three keys inside problem: the variables you decide, the constraints they must respect, and one objective. Expressions are strings, and they are linear.
- variables
- A list. Each entry needs a
nameof letters, digits and underscores, not starting with a digit.typeiscontinuous(the default),integerorbinary.lowerandupperare optional numeric bounds; omit one and the variable is unbounded on that side. - constraints
- A list of
exprstrings, each with an optionalnamethat comes back in the diagnosis. Anexpris terms on the left, one number on the right, joined by<=,>=or==. - objective
senseisminormax;expruses the same terms with no comparison and no constant. A constant only shifts the value, so it is refused rather than silently dropped.
Terms. A term is a coefficient, an asterisk and a variable name: 4*chairs. A coefficient of one may be written as the name alone. Terms join with a plus or a minus and a space either side: 4*chairs + 8*tables - 2*offcuts. Anything that is not linear is not accepted: no product of two variables, no functions, no exponents.
Declaring a variable integer or binary is what turns a linear program into a mixed-integer one. Nothing else in the envelope changes, and the price is still the base fee plus the seconds. The worked example below is a complete model with five binary variables.
Status and outcome
Two frozen vocabularies. status is the lifecycle of the problem; once it reaches finished, outcome classifies the answer.
| Status | Meaning |
|---|---|
| pending_input | Accepted, still waiting for the model you are uploading separately. |
| queued | Accepted and waiting to be solved. |
| running | An engine has picked it up and is solving. |
| finished | An answer exists (see outcome). The solution is ready to fetch. |
| failed | The service could not run the solve; error_code says why. A model the chosen solver cannot handle is finished instead (outcome error). |
| cancelled | Cancelled by you, or expired while queued. |
| Outcome | Meaning |
|---|---|
| solved | The answer that was asked for: an optimal or within-tolerance solution. result holds the point, objective_value is present. |
| no_solution | Proven: no answer exists as asked (infeasible or unbounded). termination_status carries the exact diagnosis. |
| limit | A budget stopped the solve first (time, memory, iterations). If an incumbent exists, result holds it and objective_value is present. |
| error | The solver ran and broke on this model: numerical failure, invalid model, or a constraint class this engine cannot express. Still an answer about this model. Not retried. Pick another solver or reformulate. |
Branch on these two fields only. The solution also carries the solver's verbatim MOI termination_status (such as OPTIMAL or TIME_LIMIT) as diagnosis detail; treat it as display text, not a contract.
Idempotency
Set options.idempotency_key to a unique string. If a network blip makes you retry, the second submit returns the original problem instead of creating a duplicate (and a duplicate credit hold).
"options": { "idempotency_key": "order-4821-solve", "time_limit_seconds": 5 }
Keys are scoped to your account. Reusing one always returns the first problem created with it.
Errors
Failures come back with the matching HTTP status and a JSON body. Match on code, show message, and read details when a field is at fault.
{ "error": { "code": "invalid_envelope", "message": "...", "details": {...} } }
| HTTP | Code | When |
|---|---|---|
| 401 | invalid_key | Missing, unknown or revoked bearer key. Answered in the usual error envelope, with a WWW-Authenticate: Bearer header. |
| 402 | insufficient_credits | The maximum cost of the submission is more than the balance available. Lower time_limit_seconds, or choose a cheaper compute preset. |
| 403 | customer_suspended | The account is suspended. |
| 404 | not_found | No such problem, solution, or key for this customer. |
| 409 | last_key | Refused: you cannot revoke your only API key. |
| 410 | purged | The solution payload is past its retention window. |
| 413 | envelope_too_large | The request body exceeds 20 MB. |
| 422 | invalid_envelope | The envelope failed validation. details lists the fields. |
| 422 | unknown_solver | The requested solver is not in the catalogue. |
| 422 | compute_preset_not_available | That compute preset does not exist, or the requested solver does not run on it. |
| 429 | rate_limited | Too many requests for this account. Retry-After says how long to wait. |
Limits and credits
Two things bound your usage: how many solves run at once, and how long each may run.
- Concurrency
- Your
concurrency_cap(seeGET /account) is how many problems solve at the same time. Beyond it, new submits wait in the queue. - Credits
- A solve costs its engine's base fee plus its rate on the compute preset it runs on, per second. The maximum cost is that rate over the whole of
time_limit_seconds, and it is reserved when the problem is admitted;GET /quoteanswers the same figure before you submit. Settlement bills the seconds actually used, so a run that finishes early costs less and a cancelled one bills what it ran.
Many solves at once? GET /events carries the whole account on one connection, and ?problems=a,b,c narrows it to as many as 100. For the answer itself, a webhook costs you no request at all.
Problems
Submit optimisation problems and follow them to a result.
POST
/problems
Submit one envelope. Returns the id and the effective limits.
- envelopebody
- A versioned submit envelope (see The envelope).
curl -X POST "$BASE/problems" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @envelope.json
{
"problem_id": "prb_9f3c...",
"status": "queued",
"time_limit_seconds": 5,
"pricing": {
"compute_preset": "cpu-standard",
"max_runtime_seconds": 5,
"max_cost": 6,
"rates": {"highs": {"base_fee": 1, "effective_rate": 1.0}}
}
}
GET
/problems/{id}
Status, queue position, live progress, and the solution link when ready.
curl "$BASE/problems/prb_9f3c..." \
-H "Authorization: Bearer $API_KEY"
{
"problem_id": "prb_9f3c...",
"status": "running",
"progress": {"gap": 0.03, "incumbent": 1690.0},
"outcome": null,
"solution_url": null
}
POST
/problems/{id}/wait
Hold the request open until the problem is terminal, then answer its state.
- timeout_secondsquery
- How long to hold, 1 to 60 seconds, 60 by default. On expiry you get the state as it stands, not an error, so a solve longer than the cap needs the call repeated.
curl -X POST "$BASE/problems/prb_9f3c.../wait?timeout_seconds=60" \
-H "Authorization: Bearer $API_KEY"
{
"problem_id": "prb_9f3c...",
"status": "finished",
"outcome": "solved",
"termination_status": "OPTIMAL",
"usage": {"billable_seconds": 0.4, "wall_seconds": 0.4,
"compute_preset": "cpu-standard"},
"cost": {"credits": 2, "settled_at": "2026-09-18T09:12:05Z"}
}
GET
/problems/{id}/events
Server-sent events for one problem. Drop the id segment and call /events for the whole account.
- kindsquery
- Any of status, progress and log, comma separated. All three on one problem; the account stream carries status and progress, and refuses log.
- problemsquery
- On /events only: up to 100 comma-separated ids. The whole account when you leave it out.
- Last-Event-IDheader
- The id of the last event you handled. The stream resumes from it, so a dropped connection loses nothing.
# one problem
curl -N "$BASE/problems/prb_9f3c.../events" \
-H "Authorization: Bearer $API_KEY"
# every problem on the account, on one connection
curl -N "$BASE/events" -H "Authorization: Bearer $API_KEY"
event: problem.updated
id: 1758306411.4.0
data: {"v":1,"type":"problem.updated","problem_id":"prb_9f3c...",
"data":{"status":"running","outcome":null}}
event: problem.updated
id: 1758306413.5.0
data: {"v":1,"type":"problem.updated","problem_id":"prb_9f3c...",
"data":{"status":"finished","outcome":"solved"}}
GET
/problems
List your problems, or read many at once with ?ids=a,b,c.
- idsquery
- Comma-separated ids for a direct multi-get (200 at most).
- statusquery
- Filter by lifecycle status.
- tagquery
- Filter by a tag you set in options.tags.
- limit / offsetquery
- Page window (limit 200 at most).
# recent problems, newest first
curl "$BASE/problems?status=running&limit=20" \
-H "Authorization: Bearer $API_KEY"
# bulk status: one call for many ids (<=200)
curl "$BASE/problems?ids=prb_a,prb_b,prb_c" \
-H "Authorization: Bearer $API_KEY"
{
"problems": [
{"problem_id": "prb_a", "status": "finished", ...},
{"problem_id": "prb_b", "status": "running", ...}
]
}
POST
/problems/{id}/cancel
Cancel a queued or running problem. Time already consumed stays billable.
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
-H "Authorization: Bearer $API_KEY"
{"problem_id": "prb_9f3c...", "status": "cancelled"}
Solutions
Retrieve results, one at a time or in bulk.
GET
/problems/{id}/result
The full solution envelope. Fetching it completes delivery.
# 302 to a presigned URL by default; inline=1 returns the body itself.
curl "$BASE/problems/prb_9f3c.../result?inline=1" \
-H "Authorization: Bearer $API_KEY"
{
"problem_id": "prb_9f3c...",
"outcome": "solved",
"solution": {
"solver_used": "highs",
"outcome": "solved",
"termination_status": "OPTIMAL",
"objective_value": 1750.0,
"result": {"values": {"chairs": 30.0, "tables": 5.0}},
"metering": {"wall_seconds": 0.4}
}
}
Solvers and account
The engine catalogue and your account state.
GET
/solvers
The engine catalogue with each engine's prices, and every compute preset a submission may name.
curl "$BASE/solvers" -H "Authorization: Bearer $API_KEY"
{
"solvers": [
{"name": "highs", "display_name": "HiGHS",
"description": "LP, MIP", "availability": "ok",
"pricing": {
"base_fee": 1, "runtime_rate": 1.0,
"default_preset": "cpu-standard",
"presets": [
{"code": "cpu-standard", "name": "CPU Standard",
"multiplier": 1.0, "is_default": true,
"effective_rate": 1.0},
{"code": "cpu-performance", "name": "CPU Performance",
"multiplier": 2.5, "is_default": false,
"effective_rate": 2.5}
]}}
],
"compute_presets": [
{"code": "cpu-standard", "name": "CPU Standard",
"description": "2 vCPU, 4 GiB memory", "is_gpu": false}
]
}
GET
/quote
What a submission would cost, from the rule the intake holds against. Same engine, same preset, same runtime, same number.
- solverquery
- Required. An engine or a meta-solver name.
- compute_presetquery
- Optional. Defaults to that engine's own default preset.
- time_limit_secondsquery
- Optional. Defaults to the platform default and is capped at the platform maximum.
# what a 60 second solve on HiGHS would cost, on the larger CPU preset
curl "$BASE/quote?solver=highs&compute_preset=cpu-performance&time_limit_seconds=60" \
-H "Authorization: Bearer $API_KEY"
{
"solver": "highs",
"compute_preset": "cpu-performance",
"max_runtime_seconds": 60,
"max_cost": 151,
"rates": {"highs": {"base_fee": 1, "effective_rate": 2.5}}
}
max_cost is what a submission on these terms reserves: the dearest candidate running the whole runtime. A meta-solver answers one rate per member engine that offers the preset.
GET
/account
Credit balance, concurrency cap, account status.
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
{
"name": "Acme Corp",
"status": "active",
"concurrency_cap": 4,
"credit_balance": 4820,
"credit_available": 4770
}
Webhooks
Configure and inspect your outbound callbacks. See the Webhooks guide for signatures.
GET
/account/webhook
Read your callback URL, your event filters, and the first characters of the signing secret. The secret itself is never returned; rotate if you have lost it.
curl "$BASE/account/webhook" -H "Authorization: Bearer $API_KEY"
{
"url": "https://acme.example/hook",
"events": ["terminal"],
"secret_prefix": "whsec_<first 8>"
}
PUT
/account/webhook
Set or clear the callback URL, and choose the events it receives. Validated (https, reachable) on save.
- urlbody
- The https endpoint, or an empty string to clear it.
curl -X PUT "$BASE/account/webhook" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://acme.example/hook", "events": ["terminal", "progress"]}'
{
"url": "https://acme.example/hook",
"events": ["terminal", "progress"],
"secret_prefix": "whsec_<first 8>"
}
POST
/account/webhook/rotate
Mint a fresh signing secret. The previous one keeps signing for 24 hours, so deliveries in flight still verify.
curl -X POST "$BASE/account/webhook/rotate" \
-H "Authorization: Bearer $API_KEY"
{
"url": "https://acme.example/hook",
"events": ["terminal"],
"secret_prefix": "whsec_<first 8>",
"secret": "whsec_<the full secret, shown once>"
}
POST
/account/webhook/test
Send a signed sample event to your endpoint right now.
curl -X POST "$BASE/account/webhook/test" \
-H "Authorization: Bearer $API_KEY"
{
"id": "dlv_4c1e...",
"event_id": "evt_8a02...",
"type": "test",
"status": "delivered",
"attempts": [{"at": "2026-09-18T09:12:04Z", "http_status": 200, "error": null}]
}
GET
/account/webhook/deliveries
Recent delivery attempts with their status and last error.
curl "$BASE/account/webhook/deliveries" \
-H "Authorization: Bearer $API_KEY"
{
"deliveries": [
{"id": "dlv_4c1e...", "event_id": "evt_8a02...",
"type": "problem.updated", "problem_id": "prb_a",
"subscriber": "account", "url": "https://acme.example/hook",
"status": "dead", "attempts": [{"at": "2026-09-18T09:12:04Z",
"http_status": 500, "error": "server error"}]}
],
"next_cursor": null
}
POST
/account/webhook/deliveries/{id}/retry
Re-send a dead-lettered delivery.
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
-H "Authorization: Bearer $API_KEY"
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
API keys
Manage keys programmatically. The first key is created in the portal.
GET
/account/keys
List your keys by prefix. The secret is never shown again.
curl "$BASE/account/keys" -H "Authorization: Bearer $API_KEY"
{
"keys": [
{"id": 41, "name": "prod", "prefix": "3f9c1a2b",
"created_at": "2026-07-01T09:00:00Z", "expires_at": null}
]
}
POST
/account/keys
Mint a new key. The secret is returned exactly once.
- namebody
- A label for the key (optional).
- expires_daysbody
- Days until expiry, or omit for none.
curl -X POST "$BASE/account/keys" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "prod-2026", "expires_days": 365}'
{"id": 42, "name": "prod-2026", "prefix": "7b2e4f9d",
"key": "<full key, shown once>"}
DELETE
/account/keys/{id}
Revoke a key. Your last remaining key is protected (409).
curl -X DELETE "$BASE/account/keys/41" \
-H "Authorization: Bearer $API_KEY"
{"revoked": 41}
Webhooks
Have us call you. Set options.webhook per problem, or a default endpoint on your account, and receive a signed event the moment a problem finishes. This is the cheapest way to run in production: it costs you no request of your own.
Events. A subscription chooses what it receives through options.webhook.events, per problem or on the account default. terminal, the default, sends problem.updated on a terminal status and the problem.delivered that follows the first download. running sends every problem.updated. progress sends problem.progress while the solve runs, each tick a cumulative snapshot where the last one wins. settled sends problem.settled, once the cost is known. Payloads are thin (status, outcome, termination_status and a solution_url), so fetch the body with one call. Log lines are never delivered to a webhook: they exist on the per-problem stream alone.
Verify every delivery. We sign with the Standard Webhooks scheme, so a library you already have may do this for you. Three headers carry it: webhook-id, webhook-timestamp, and webhook-signature, the last a base64 HMAC-SHA256 over {id}.{timestamp}.{body} under your signing secret, spelled v1,.... Refuse a timestamp more than five minutes old, and accept any one of the space-separated values: a rotation signs with both secrets for 24 hours, so a receiver that has not picked up the new one still finds a value it can verify.
import base64, hashlib, hmac, json, os, time
# The Standard Webhooks scheme. The secret is shown once by /account/webhook and
# spells its key in base64 after a whsec_ prefix; sign with the decoded bytes.
SECRET = os.environ["SOLVE_WEBHOOK_SECRET"] # "whsec_<base64>"
KEY = base64.b64decode(SECRET.removeprefix("whsec_"))
TOLERANCE = 300 # five minutes, as we send
def handle(request):
body = request.get_data() # the raw bytes, unparsed
msg_id = request.headers["webhook-id"] # "evt_..." and the dedupe key
sent_at = int(request.headers["webhook-timestamp"])
if abs(time.time() - sent_at) > TOLERANCE: # refuse a replayed delivery
return "stale timestamp", 400
signed = f"{msg_id}.{sent_at}.".encode() + body
expect = "v1," + base64.b64encode(
hmac.new(KEY, signed, hashlib.sha256).digest()
).decode()
# A rotation signs with both secrets for 24 hours, so the header may carry
# several space-separated values and any one of them matching is enough.
presented = request.headers["webhook-signature"].split(" ")
if not any(hmac.compare_digest(expect, value) for value in presented):
return "bad signature", 400
event = json.loads(body)
if event["type"] == "problem.updated" and event["data"]["status"] == "finished":
fetch_result(event["problem_id"]) # payloads are thin: pull the body
# every other type ("problem.delivered", "problem.progress", "problem.settled",
# "test") just needs the 200 back
return "", 200
Retries. A delivery retries with backoff for 24 hours, then is marked dead. Inspect the log at GET /account/webhook/deliveries and replay a dead one with POST /account/webhook/deliveries/{delivery_id}/retry, from the portal or your own code; rows are kept 90 days. The same event may arrive more than once, so treat webhook-id as the dedupe key.
Managing API keys
Your first key is created in the portal (you need a key to call the API). After that, manage them over the API so rotation can be automated.
Rotate with no downtime. Create the successor, deploy it, then revoke the old key. Revoking your only key is refused so you cannot lock yourself out.
# Rotate with no downtime: mint the successor, switch over, then revoke.
# 1. Create the replacement (the secret is shown exactly once).
curl -s -X POST "$BASE/account/keys" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "prod-2026"}'
# -> {"id":42,"name":"prod-2026","prefix":"7b2e4f9d","key":"<full key, shown once>"}
# 2. Deploy the new key everywhere, then revoke the old one by id.
curl -s -X DELETE "$BASE/account/keys/41" -H "Authorization: Bearer $NEW_KEY"
# -> {"revoked": 41}
Python SDK
Write the model in Python and get the solution back as objects, with submit, wait, webhook verification and idempotent retries wrapped for you. The Studio runs it in the browser, so you can read the shape before you wire anything up.
The envelope is the contract, so the SDK stays a thin convenience over it. Anything jumpy does, your own client can do.
Worked examples
A production mix (LP) is the quickstart above. Here is the same envelope with integer decisions: five candidate projects, one budget, pick the subset worth the most.
{
"api_version": "1",
"problem": {
"variables": [
{
"name": "project_1",
"type": "binary"
},
{
"name": "project_2",
"type": "binary"
},
{
"name": "project_3",
"type": "binary"
},
{
"name": "project_4",
"type": "binary"
},
{
"name": "project_5",
"type": "binary"
}
],
"constraints": [
{
"expr": "12*project_1 + 5*project_2 + 8*project_3 + 21*project_4 + 9*project_5 <= 30",
"name": "budget"
}
],
"objective": {
"sense": "max",
"expr": "18*project_1 + 6*project_2 + 12*project_3 + 30*project_4 + 11*project_5"
}
},
"solver": "highs",
"options": {
"compute_preset": "cpu-standard",
"time_limit_seconds": 5,
"tags": [
"budget"
]
}
}
Post it exactly like the quickstart. The answer picks project_3 and project_4, spends 29 of the 30, and reports objective_value 42 with termination_status OPTIMAL.
A delivery route (VRP) and a shift cover are larger than a code panel, so they live in the Studio, written in Python and submitted through the same envelope.
Try them in the Studio
Every example loads into the live editor with one click. No key required to run the sandbox.
Your first solve is five minutes away
Run a real model against a real solver, in the browser. No account, no credit card.