Overslaan naar inhoud

De NexOR Optimization API

Dien een probleem in, krijg een oplossing. Eén REST-call erin, één antwoord eruit, stabiel onder /solve/v1 en geauthenticeerd met een bearer-sleutel. Elk endpoint op deze pagina draait vandaag; de Python-SDK is een preview en staat als zodanig aangeduid.

Free tier, geen kaart nodig. Uw eerste solve is een kwestie van minuten.

  1. POST /problems
  2. queued het probleem heeft een id
  3. POST /problems/{id}/wait
  4. running hier blijft de wait-call openstaan
  5. finished de wait keert terug, of uw webhook gaat af
  6. GET /problems/{id}/result
  7. solution u hebt het antwoord
Aan de slag

Een solver die u over HTTP aanroept

U stuurt een wiskundig optimalisatieprobleem als JSON. Wij draaien het op onze solvers en geven de oplossing terug. Het is domeinonafhankelijk: lineaire programma's, gemengd geheeltallige modellen, routering, scheduling, wat u ook kunt uitdrukken, alles reist in dezelfde envelope.

Elke integratie kent dezelfde drie bewegingen: verstuur een probleem en krijg een id, wacht op één aanvraag of laat een ondertekende webhook u bereiken, en haal daarna de oplossing op. De volgende sectie doet alle drie in minder dan een minuut.

Wij behandelen de body van uw probleem als opaak. De manager valideert de envelope en meet het rekenverbruik, maar leest uw model nooit. Dat is wat één API generiek houdt voor elke probleemklasse.

Aan de slag

Uw eerste solve, in drie stappen

Van nul naar een echt antwoord in enkele minuten. Een productiemix met twee producten: maximaliseer de marge binnen limieten op machine-uren en materiaal. Het optimum is 30 stoelen en 5 tafels, doelfunctiewaarde 1750.

Test het eerst live

Geen account, geen sleutel, gratis. Schrijf een model in Python en laat het lopen op de echte solver, in uw browser.

  1. Kopieer een API-sleutel

    Geef hem een naam en kies een vervaldatum, meer stelt het formulier niet voor. Deze sleutel identificeert u bij elke API-call die u doet. Het geheim wordt maar één keer getoond, kopieer het dus meteen. Dit is de enige stap die in de browser gebeurt.

    API-sleutels openen
  2. Indienen, wachten, ophalen

    Plak uw sleutel in het voorbeeld hieronder en voer het uit: dien de envelope in, houd één aanvraag open tot het probleem klaar is, en haal dan de oplossing op. Dat is de kortste weg vanaf een laptop, waar niets een webhook kan ontvangen. Zodra u een server draait, registreer een webhook en laat de middelste stap vallen.

# 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, ...}
De envelope.json die hierboven gebruikt wordt
envelope.json
{
  "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"
    ]
  }
}

Klaar voor de details? Volledige API-referentie

Aan de slag

Bearer-sleutels

Elke aanvraag draagt uw API-sleutel als bearer-token. Sleutels zijn afgebakend tot de optimalisatie-API, dus een gelekte sleutel kan niets anders in uw account raken.

Authorization: Bearer <your-api-key>

Maak uw eerste sleutel aan in het portaal. Daarna kunt u sleutels oplijsten, aanmaken en intrekken via de API zelf (zie API-sleutels). Bewaar sleutels server-side en roteer ze zonder downtime wanneer nodig.

Kernbegrippen

Hoe een solve verloopt

Oplossen verloopt asynchroon. Een inzending geeft meteen een id terug met status queued; de solve draait op onze infrastructuur; de uitkomst komt naar u toe.

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

Drie manieren om te weten dat het klaar is. Een webhook bereikt u op het moment dat het probleem klaar is en kost u geen aanvraag; dat is wat een server hoort te gebruiken. POST /problems/{id}/wait houdt tot dan één aanvraag open, dat is wat een script op een laptop hoort te gebruiken. GET /problems/{id}/events streamt dezelfde events over één verbinding en draagt de voortgang van de solver terwijl hij werkt. Elke payload is beknopt, dus de oplossing haalt u nog steeds op met één call.

Kernbegrippen

De inzendingsenvelope

Eén geversioneerde wrapper rond vier onderdelen. Wij beheren en valideren de wrapper; het problem erin is van u.

api_version
De wire-versie. Vandaag altijd "1".
problem
Uw model: variabelen, beperkingen, een doelfunctie. Opaak voor ons, gevalideerd door de solver.
solver
De solver die moet draaien: een concrete solvernaam, of de naam van een meta-solver, dat is een benoemde set solvers waarvan om het even welke de opdracht kan opnemen. Solverinstellingen reizen mee binnen problem.
options
time_limit_seconds, compute_preset, webhook, idempotency_key en tags. Allemaal optioneel.
options.compute_preset
De rekenkracht die de solve reserveert, per code (cpu-standard, gpu-standard). Laat u het weg, dan draait de solver op zijn eigen standaardprofiel. GET /solvers lijst elk profiel op en welke solvers het aanbieden.
envelope.json
{
  "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"
    ]
  }
}

Machineleesbare JSON-schema's: admission_request_v1.json

Het schema typeert problem als een opaak object, want de manager leest het nooit. De sectie hieronder is de vorm die de solvers wel lezen.

Kernbegrippen

Een model beschrijven

Drie sleutels binnen problem: de variabelen waarover u beslist, de constraints die ze moeten respecteren, en één doelfunctie. Expressies zijn strings, en ze zijn lineair.

variables
Een lijst. Elk item heeft een name nodig van letters, cijfers en underscores, niet beginnend met een cijfer. type is continuous (de standaard), integer of binary. lower en upper zijn optionele numerieke grenzen; laat er één weg en de variabele is aan die kant onbegrensd.
constraints
Een lijst van expr-strings, elk met een optionele name die terugkomt in de diagnose. Een expr is termen links, één getal rechts, verbonden door <=, >= of ==.
objective
sense is min of max; expr gebruikt dezelfde termen, zonder vergelijking en zonder constante. Een constante verschuift alleen de waarde, dus wordt ze geweigerd in plaats van stil weggelaten.

Termen. Een term is een coëfficiënt, een sterretje en een variabelenaam: 4*chairs. Een coëfficiënt van één mag u schrijven als de naam alleen. Termen worden verbonden met een plus of een min en een spatie aan weerszijden: 4*chairs + 8*tables - 2*offcuts. Wat niet lineair is, wordt niet aanvaard: geen product van twee variabelen, geen functies, geen exponenten.

Een variabele integer of binary verklaren is wat een lineair programma in een gemengd geheeltallig programma verandert. Verder verandert er niets in de envelope, en de prijs blijft het basistarief plus de seconden. De uitgewerkt voorbeeld hieronder staat een volledig model met vijf binaire variabelen.

Kernbegrippen

Status en uitkomst

Twee vaste woordenlijsten. status is de levenscyclus van het probleem; zodra het finished bereikt, duidt outcome het antwoord.

StatusBetekenis
pending_inputAanvaard, nog in afwachting van het model dat u apart oplaadt.
queuedAanvaard en wacht op een oplossing.
runningEen solver heeft het opgenomen en is aan het oplossen.
finishedEr is een antwoord (zie outcome). De oplossing is klaar om op te halen.
failedDe dienst kon de oplossing niet uitvoeren; error_code zegt waarom. Een model dat de gekozen solver niet aankan komt in plaats daarvan op finished (outcome error).
cancelledDoor u geannuleerd, of vervallen terwijl het in de wachtrij stond.
UitkomstBetekenis
solvedHet antwoord waar om gevraagd werd: een optimale oplossing of een oplossing binnen de tolerantie. result bevat het punt, objective_value is aanwezig.
no_solutionBewezen: er bestaat geen antwoord zoals gevraagd (onhaalbaar of onbegrensd). termination_status draagt de exacte diagnose.
limitEen budget stopte de solve eerst (tijd, geheugen, iteraties). Bestaat er een incumbent, dan bevat result die en is objective_value aanwezig.
errorDe solver liep en brak op dit model: numeriek falen, een ongeldig model, of een constraintklasse die deze solver niet kan uitdrukken. Toch een antwoord over dit model. Wordt niet opnieuw geprobeerd. Kies een andere solver of herformuleer.

Vertak enkel op deze twee velden. De oplossing bevat ook de letterlijke MOI termination_status van de solver (zoals OPTIMAL of TIME_LIMIT) als diagnosedetail; behandel die als weergavetekst, niet als contract.

Kernbegrippen

Idempotentie

Stel options.idempotency_key in op een unieke string. Als u door een netwerkhapering opnieuw indient, geeft de tweede submit het oorspronkelijke probleem terug in plaats van een duplicaat aan te maken (en een dubbele kredietreservering).

"options": { "idempotency_key": "order-4821-solve", "time_limit_seconds": 5 }

Sleutels zijn gebonden aan uw account. Een sleutel hergebruiken geeft altijd het eerste probleem terug dat ermee werd aangemaakt.

Kernbegrippen

Fouten

Fouten komen terug met de passende HTTP-status en een JSON-body. Match op code, toon message en lees details wanneer een veld in fout is.

{ "error": { "code": "invalid_envelope", "message": "...", "details": {...} } }
HTTPCodeWanneer
401invalid_keyOntbrekende, onbekende of ingetrokken bearer-sleutel. Beantwoord in de gewone foutenvelope, met een WWW-Authenticate: Bearer-header.
402insufficient_creditsDe maximale kost van de inzending is hoger dan het beschikbare saldo. Verlaag time_limit_seconds, of kies een goedkoper rekenprofiel.
403customer_suspendedHet account is geschorst.
404not_foundGeen dergelijk probleem, dergelijke oplossing of sleutel voor deze klant.
409last_keyGeweigerd: u kunt uw enige API-sleutel niet intrekken.
410purgedDe payload van de oplossing is voorbij haar bewaartermijn.
413envelope_too_largeDe body van de aanvraag is groter dan 20 MB.
422invalid_envelopeDe envelope is niet door de validatie geraakt. details lijst de velden op.
422unknown_solverDe gevraagde solver staat niet in de catalogus.
422compute_preset_not_availableDat rekenprofiel bestaat niet, of de gevraagde solver draait er niet op.
429rate_limitedTe veel aanvragen voor dit account. Retry-After zegt hoe lang u moet wachten.
Kernbegrippen

Limieten en credits

Twee dingen begrenzen uw gebruik: hoeveel solves tegelijk draaien, en hoe lang elk mag draaien.

Gelijktijdigheid
Uw concurrency_cap (zie GET /account) bepaalt hoeveel problemen tegelijk opgelost worden. Daarboven wachten nieuwe submits in de wachtrij.
Credits
Een solve kost het basistarief van zijn solver plus zijn tarief op het rekenprofiel waarop hij draait, per seconde. De maximale kost is dat tarief over de volledige time_limit_seconds, en wordt gereserveerd zodra het probleem toegelaten is; GET /quote geeft hetzelfde cijfer vóór u indient. De afrekening factureert de werkelijk gebruikte seconden, dus een run die vroeger klaar is kost minder en een geannuleerde run factureert wat ze gedraaid heeft.

Veel solves tegelijk? GET /events draagt het hele account op één verbinding, en ?problems=a,b,c beperkt het tot maximaal 100. Voor het antwoord zelf kost een webhook u geen enkele aanvraag.

API-referentie

Problemen

Dien optimalisatieproblemen in en volg ze tot een resultaat.

POST /problems

Dien één envelope in. Geeft het id en de effectieve limieten terug.

envelopebody
Een geversioneerde submit-envelope (zie De envelope).
Aanvraag
curl -X POST "$BASE/problems" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @envelope.json
Antwoord
{
  "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, positie in de wachtrij, live voortgang, en de link naar de oplossing zodra ze klaar is.

Aanvraag
curl "$BASE/problems/prb_9f3c..." \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "problem_id": "prb_9f3c...",
  "status": "running",
  "progress": {"gap": 0.03, "incumbent": 1690.0},
  "outcome": null,
  "solution_url": null
}

POST /problems/{id}/wait

Houd de aanvraag open tot het probleem terminaal is. Geeft daarna zijn toestand terug.

timeout_secondsquery
Hoe lang open blijven, 1 tot 60 seconden, standaard 60. Bij afloop krijgt u de toestand zoals ze is, geen fout, dus een solve die langer duurt dan het plafond vraagt een herhaalde call.
Aanvraag
curl -X POST "$BASE/problems/prb_9f3c.../wait?timeout_seconds=60" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "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 voor één probleem. Laat het id-segment weg en roep /events aan voor het hele account.

kindsquery
Een of meer van status, progress en log, gescheiden door komma's. Alle drie op één probleem; de stroom van de account draagt status en progress, en weigert log.
problemsquery
Alleen op /events: tot 100 door komma's gescheiden ids. Het hele account als u het weglaat.
Last-Event-IDheader
Het id van het laatste event dat u verwerkt hebt. De stream hervat daarvandaan, zodat een verbroken verbinding niets verliest.
Aanvraag
# 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"
Antwoord
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

Lijst uw problemen op, of lees er meerdere tegelijk met ?ids=a,b,c.

idsquery
Door komma's gescheiden ids voor een directe multi-get (maximaal 200).
statusquery
Filter op status in de levenscyclus.
tagquery
Filter op een tag die u instelt in options.tags.
limit / offsetquery
Paginavenster (limiet maximaal 200).
Aanvraag
# 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"
Antwoord
{
  "problems": [
    {"problem_id": "prb_a", "status": "finished", ...},
    {"problem_id": "prb_b", "status": "running", ...}
  ]
}

POST /problems/{id}/cancel

Annuleer een probleem in de wachtrij of in uitvoering. Reeds verbruikte tijd blijft factureerbaar.

Aanvraag
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{"problem_id": "prb_9f3c...", "status": "cancelled"}
API-referentie

Oplossingen

Haal resultaten op, één per één of in bulk.

GET /problems/{id}/result

De volledige oplossingsenvelope. Ze ophalen voltooit de levering.

Aanvraag
# 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"
Antwoord
{
  "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}
  }
}
API-referentie

Solvers en account

De solvercatalogus en de staat van uw account.

GET /solvers

De catalogus van solvers met de prijzen van elk, en elk rekenprofiel dat een inzending kan benoemen.

Aanvraag
curl "$BASE/solvers" -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "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

Wat een inzending zou kosten, volgens dezelfde regel als bij de toelating. Zelfde solver, zelfde profiel, zelfde looptijd, zelfde cijfer.

solverquery
Verplicht. De naam van een solver of een meta-solver.
compute_presetquery
Optioneel. Standaard het eigen standaardprofiel van die solver.
time_limit_secondsquery
Optioneel. Standaard de waarde van het platform, geplafonneerd op het maximum van het platform.
Aanvraag
# 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"
Antwoord
{
  "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 wat een inzending onder deze voorwaarden reserveert: de duurste kandidaat die de volledige looptijd draait. Een meta-solver antwoordt één tarief per lidsolver die het profiel aanbiedt.

GET /account

Creditsaldo, gelijktijdigheidslimiet, accountstatus.

Aanvraag
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "name": "Acme Corp",
  "status": "active",
  "concurrency_cap": 4,
  "credit_balance": 4820,
  "credit_available": 4770
}
API-referentie

Webhooks

Configureer en inspecteer uw uitgaande callbacks. Zie de webhookgids voor de handtekeningen.

GET /account/webhook

Leest uw callback-URL, uw gebeurtenisfilters en de eerste tekens van het ondertekeningsgeheim. Het geheim zelf wordt nooit teruggegeven; roteer als u het kwijt bent.

Aanvraag
curl "$BASE/account/webhook" -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "url": "https://acme.example/hook",
  "events": ["terminal"],
  "secret_prefix": "whsec_<first 8>"
}

PUT /account/webhook

Stelt de callback-URL in of wist ze, en kiest welke gebeurtenissen ze ontvangt. Gevalideerd (https, bereikbaar) bij het opslaan.

urlbody
Het https-endpoint, of een lege string om het te wissen.
Aanvraag
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"]}'
Antwoord
{
  "url": "https://acme.example/hook",
  "events": ["terminal", "progress"],
  "secret_prefix": "whsec_<first 8>"
}

POST /account/webhook/rotate

Maakt een nieuw ondertekeningsgeheim aan. Het vorige blijft 24 uur ondertekenen, zodat leveringen die onderweg zijn nog kloppen.

Aanvraag
curl -X POST "$BASE/account/webhook/rotate" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "url": "https://acme.example/hook",
  "events": ["terminal"],
  "secret_prefix": "whsec_<first 8>",
  "secret": "whsec_<the full secret, shown once>"
}

POST /account/webhook/test

Stuur nu een ondertekend voorbeeldevent naar uw endpoint.

Aanvraag
curl -X POST "$BASE/account/webhook/test" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "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

Recente leveringspogingen met hun status en laatste fout.

Aanvraag
curl "$BASE/account/webhook/deliveries" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "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

Verstuur een levering uit de dead-letter opnieuw.

Aanvraag
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
API-referentie

API-sleutels

Beheer sleutels programmatisch. De eerste sleutel maakt u aan in het portaal.

GET /account/keys

Lijst uw sleutels op per prefix. Het geheim wordt nooit opnieuw getoond.

Aanvraag
curl "$BASE/account/keys" -H "Authorization: Bearer $API_KEY"
Antwoord
{
  "keys": [
    {"id": 41, "name": "prod", "prefix": "3f9c1a2b",
     "created_at": "2026-07-01T09:00:00Z", "expires_at": null}
  ]
}

POST /account/keys

Maak een nieuwe sleutel aan. Het geheim wordt precies één keer teruggegeven.

namebody
Een label voor de sleutel (optioneel).
expires_daysbody
Aantal dagen tot vervaldatum, of weglaten voor geen.
Aanvraag
curl -X POST "$BASE/account/keys" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "prod-2026", "expires_days": 365}'
Antwoord
{"id": 42, "name": "prod-2026", "prefix": "7b2e4f9d",
 "key": "<full key, shown once>"}

DELETE /account/keys/{id}

Trek een sleutel in. Uw laatst overblijvende sleutel is beschermd (409).

Aanvraag
curl -X DELETE "$BASE/account/keys/41" \
  -H "Authorization: Bearer $API_KEY"
Antwoord
{"revoked": 41}
Gidsen

Webhooks

Laat ons u verwittigen. Stel options.webhook in per probleem, of een standaard endpoint op uw account, en ontvang een ondertekend event zodra een probleem klaar is. Dit is de goedkoopste manier om in productie te draaien: ze kost u geen enkele eigen aanvraag.

Gebeurtenissen. Een abonnement kiest via options.webhook.events wat het ontvangt, per probleem of als standaard op de account. terminal, de standaard, stuurt problem.updated bij een eindstatus en de problem.delivered die volgt op de eerste download. running stuurt elke problem.updated. progress stuurt problem.progress tijdens het oplossen, elke tik een cumulatieve momentopname waarbij de laatste geldt. settled stuurt problem.settled, zodra de kost bekend is. De payloads zijn licht (status, outcome, termination_status en een solution_url), dus haalt u de inhoud op met één oproep. Logregels worden nooit aan een webhook geleverd: ze bestaan enkel op de stroom van één probleem.

Controleer elke levering. Wij ondertekenen volgens het Standard Webhooks-schema, dat een bibliotheek die u al gebruikt wellicht voor u afhandelt. Drie headers dragen het: webhook-id, webhook-timestamp en webhook-signature, die laatste een base64 HMAC-SHA256 over {id}.{timestamp}.{body} met uw ondertekeningsgeheim, geschreven als v1,.... Weiger een tijdstempel ouder dan vijf minuten en aanvaard om het even welke van de door spaties gescheiden waarden: bij een rotatie ondertekenen beide geheimen 24 uur lang, zodat een ontvanger die het nieuwe nog niet heeft opgepikt toch een waarde vindt die klopt.

verify.py
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

Nieuwe pogingen. Een levering wordt 24 uur lang opnieuw geprobeerd met oplopende wachttijd en daarna dood gemarkeerd. Raadpleeg het logboek op GET /account/webhook/deliveries en speel een dode levering opnieuw af met POST /account/webhook/deliveries/{delivery_id}/retry, vanuit het portaal of uw eigen code; rijen blijven 90 dagen bewaard. Dezelfde gebeurtenis kan meer dan eens aankomen, gebruik webhook-id dus als ontdubbelingssleutel.

Gidsen

API-sleutels beheren

Uw eerste sleutel maakt u aan in het portaal (u hebt een sleutel nodig om de API aan te roepen). Daarna beheert u ze via de API, zodat rotatie geautomatiseerd kan worden.

Roteer zonder downtime. Maak de opvolger aan, rol die uit en trek daarna de oude sleutel in. Uw enige sleutel intrekken wordt geweigerd, zodat u zichzelf niet kunt buitensluiten.

rotate.sh
# 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}
Gidsen

Python-SDK

jumpy, onze Python-SDK

Schrijf het model in Python en krijg de oplossing terug als objecten, met versturen, wait, webhookverificatie en idempotente retries al ingepakt. De Studio draait ze in de browser, zodat u de vorm kunt lezen voor u iets aansluit.

De envelope is het contract, dus blijft de SDK een dunne gemakslaag erbovenop. Alles wat jumpy kan, kan uw eigen client ook.

Meer

Uitgewerkte voorbeelden

Een productiemix (LP) is de quickstart hierboven. Hier is dezelfde envelope met geheeltallige beslissingen: vijf kandidaat-projecten, één budget, kies de deelverzameling die het meest waard is.

budget.json
{
  "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 het precies zoals in de quickstart. Het antwoord kiest project_3 en project_4, besteedt 29 van de 30, en meldt objective_value 42 met termination_status OPTIMAL.

Een leveringsroute (VRP) en een shiftbezetting zijn groter dan een codepaneel, dus staan ze in de Studio, geschreven in Python en ingediend via dezelfde envelope.

Probeer ze in de Studio

Elk voorbeeld laadt met één klik in de live editor. Geen sleutel nodig om de sandbox uit te voeren.

Uw eerste solve is vijf minuten ver

Draai een echt model op een echte solver, in de browser. Geen account, geen kredietkaart.