Przejdź do zawartości

API optymalizacyjne NexOR

Wysyłka zadania, odbiór rozwiązania. Jedno wywołanie REST na wejściu, jedna odpowiedź na wyjściu, stabilnie pod /solve/v1 i z uwierzytelnieniem kluczem bearer. Każdy endpoint na tej stronie działa dziś; SDK w Pythonie jest podglądem i jest tak oznaczone.

Darmowy plan, bez karty. Pierwsze rozwiązanie zajmie kilka minut.

  1. POST /problems
  2. queued problem ma identyfikator
  3. POST /problems/{id}/wait
  4. running tutaj wywołanie wait pozostaje otwarte
  5. finished wait zwraca odpowiedź albo uruchamia się twój webhook
  6. GET /problems/{id}/result
  7. solution masz odpowiedź
Rozpocznij

Solver wywoływany po HTTP

Wysyła się matematyczny problem optymalizacyjny jako JSON. Uruchamiamy go na naszych solverach i oddajemy rozwiązanie. Jest niezależny od dziedziny: programy liniowe, modele całkowitoliczbowe mieszane, marszrutyzacja, harmonogramowanie, cokolwiek da się wyrazić, wszystko podróżuje tą samą kopertą.

Każda integracja składa się z tych samych trzech kroków: wyślij problem i otrzymaj id, zaczekaj na jednym żądaniu albo pozwól, by dotarł podpisany webhook, a następnie pobierz rozwiązanie. W następnej sekcji wykonasz wszystkie trzy w niecałą minutę.

Treść zadania traktujemy jako nieprzezroczystą. Menedżer waliduje kopertę i mierzy zużycie, ale nigdy nie czyta modelu. To właśnie dzięki temu jedno API pozostaje generyczne dla każdej klasy problemów.

Rozpocznij

Pierwsze obliczenie w trzech krokach

Od zera do prawdziwej odpowiedzi w kilka minut. Miks produkcyjny dwóch wyrobów: maksymalizacja marży przy limitach maszynogodzin i materiałów. Optimum to 30 krzeseł i 5 stołów, funkcja celu 1750.

Najpierw test na żywo

Bez konta, bez klucza, za darmo. Model pisze się w Pythonie i uruchamia na prawdziwym solverze, w przeglądarce.

  1. Skopiuj klucz API

    Dowolna nazwa i termin ważności, to cały formularz. Ten klucz identyfikuje konto przy każdym wywołaniu API. Sekret jest pokazywany raz, więc trzeba go od razu skopiować. To jedyny krok wykonywany w przeglądarce.

    Otwórz klucze API
  2. Wyślij, zaczekaj, pobierz

    Wystarczy wkleić klucz do poniższego przykładu i go uruchomić: wysłać kopertę, utrzymać jedno żądanie otwarte aż do końca zadania, a potem pobrać rozwiązanie. To najkrótsza droga z laptopa, gdzie nic nie odbierze webhooka. Gdy masz już serwer, zarejestruj webhook i pomiń środkowy krok.

# 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, ...}
Plik envelope.json użyty powyżej
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"
    ]
  }
}

Czas na szczegóły? Pełna dokumentacja API

Rozpocznij

Klucze bearer

Każde żądanie niesie klucz API jako token bearer. Klucze mają zakres ograniczony do API optymalizacji, więc wyciek klucza nie sięgnie niczego innego na koncie.

Authorization: Bearer <your-api-key>

Pierwszy klucz tworzy się w portalu. Później klucze można wypisywać, wydawać i unieważniać przez samo API (zob. Klucze API). Klucze trzymaj po stronie serwera i w razie potrzeby rotuj je bez przestoju.

Podstawowe pojęcia

Jak przebiega obliczenie

Rozwiązywanie jest asynchroniczne. Zgłoszenie wraca od razu z identyfikatorem i statusem w kolejce; obliczenie działa na naszej infrastrukturze; wynik przychodzi sam.

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

Trzy sposoby, aby wiedzieć, że gotowe. Podpisany webhook dociera do Ciebie w chwili zakończenia zadania i nie kosztuje żadnego żądania; tak powinien działać serwer. POST /problems/{id}/wait utrzymuje do tego momentu jedno żądanie otwarte, tak powinien działać skrypt na laptopie. GET /problems/{id}/events przesyła te same zdarzenia jednym połączeniem i niesie postęp solvera w trakcie pracy. Każdy ładunek jest lekki, więc rozwiązanie i tak pobierasz jednym wywołaniem.

Podstawowe pojęcia

Koperta zgłoszenia

Jedna wersjonowana koperta obejmująca cztery części. Koperta jest nasza i my ją walidujemy; problem w środku należy do Państwa.

api_version
Wersja protokołu. Dziś zawsze "1".
problem
Model klienta: zmienne, ograniczenia, funkcja celu. Nieprzezroczysty dla nas, walidowany przez solver.
solver
Silnik do uruchomienia: nazwa konkretnego solvera albo nazwa meta-solvera, czyli nazwanego zbioru silników członkowskich, z których każdy może przyjąć zadanie. Strojenie solvera podróżuje wewnątrz problem.
options
time_limit_seconds, compute_preset, webhook, idempotency_key i tags. Wszystkie opcjonalne.
options.compute_preset
Zasoby, które rezerwuje obliczenie, podane kodem (cpu-standard, gpu-standard). Pomiń je, a silnik uruchomi się na własnym profilu domyślnym. GET /solvers wymienia wszystkie profile i silniki, które je oferują.
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"
    ]
  }
}

Schematy JSON czytelne maszynowo: admission_request_v1.json

Schemat opisuje problem jako obiekt nieprzezroczysty, bo menedżer nigdy go nie czyta. Poniższa sekcja to postać, którą czytają solvery.

Podstawowe pojęcia

Opis modelu

Trzy klucze wewnątrz problem: zmienne decyzyjne, ograniczenia, które muszą spełniać, i jedna funkcja celu. Wyrażenia są łańcuchami znaków i są liniowe.

variables
Lista. Każdy wpis wymaga nazwy name z liter, cyfr i podkreśleń, niezaczynającej się od cyfry. type to continuous (domyślnie), integer albo binary. lower i upper to opcjonalne granice liczbowe; pominięcie jednej zostawia zmienną nieograniczoną z tej strony.
constraints
Lista łańcuchów expr, każdy z opcjonalną nazwą name, która wraca w diagnozie. expr to składniki po lewej, jedna liczba po prawej, połączone przez <=, >= albo ==.
objective
sense przyjmuje wartość min albo max; expr korzysta z tych samych składników, bez porównania i bez stałej. Stała jedynie przesuwa wartość, dlatego jest odrzucana, a nie po cichu pomijana.

Składniki. Składnik to współczynnik, gwiazdka i nazwa zmiennej: 4*chairs. Współczynnik równy jeden można zapisać samą nazwą. Składniki łączy plus albo minus ze spacją po obu stronach: 4*chairs + 8*tables - 2*offcuts. Nic, co nie jest liniowe, nie zostanie przyjęte: żadnych iloczynów dwóch zmiennych, żadnych funkcji, żadnych potęg.

Zadeklarowanie zmiennej jako integer albo binary zamienia program liniowy w mieszany całkowitoliczbowy. Nic innego w kopercie się nie zmienia, a cena to nadal opłata podstawowa plus sekundy. Gotowy przykład poniżej to pełny model z pięcioma zmiennymi binarnymi.

Podstawowe pojęcia

Status i wynik

Dwa zamrożone słowniki. status to cykl życia problemu; gdy osiągnie finished, outcome klasyfikuje odpowiedź.

StatusZnaczenie
pending_inputPrzyjęte, wciąż czeka na model, który wysyłasz osobno.
queuedPrzyjęte i czeka na rozwiązanie.
runningSilnik podjął zadanie i je rozwiązuje.
finishedOdpowiedź istnieje (zob. outcome). Rozwiązanie jest gotowe do pobrania.
failedUsługa nie mogła wykonać rozwiązania; error_code mówi dlaczego. Model, którego wybrany solver nie obsłuży, kończy się jako finished (outcome error).
cancelledAnulowane przez klienta albo wygasłe w kolejce.
WynikZnaczenie
solvedOdpowiedź, o którą pytano: rozwiązanie optymalne albo mieszczące się w tolerancji. W result znajduje się punkt, objective_value jest obecne.
no_solutionDowiedzione: dla tak postawionego pytania odpowiedź nie istnieje (sprzeczność albo brak ograniczenia). Dokładną diagnozę niesie termination_status.
limitObliczenie zatrzymał limit (czas, pamięć, iteracje). Jeśli istnieje bieżące najlepsze rozwiązanie, znajduje się w result, a objective_value jest obecne.
errorSolver ruszył i poległ na tym modelu: błąd numeryczny, nieprawidłowy model albo klasa ograniczeń, której ten silnik nie wyrazi. To nadal odpowiedź o tym modelu. Bez ponowienia. Trzeba wybrać inny solver albo przeformułować model.

Rozgałęziaj logikę wyłącznie na tych dwóch polach. Rozwiązanie niesie też dosłowny MOI termination_status solvera (np. OPTIMAL lub TIME_LIMIT) jako szczegół diagnostyczny; traktuj go jako tekst do wyświetlenia, nie jako kontrakt.

Podstawowe pojęcia

Idempotencja

Wystarczy ustawić options.idempotency_key na unikalny łańcuch znaków. Jeśli chwilowy problem z siecią wymusi ponowienie, drugie zgłoszenie zwróci pierwotny problem zamiast tworzyć duplikat (i drugą blokadę kredytów).

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

Klucze są przypisane do konta. Ponowne użycie tego samego klucza zawsze zwraca pierwszy problem z nim utworzony.

Podstawowe pojęcia

Błędy

Niepowodzenia wracają z pasującym statusem HTTP i treścią JSON. Dopasowuj po code, wyświetlaj message, a gdy winne jest konkretne pole, sięgnij do details.

{ "error": { "code": "invalid_envelope", "message": "...", "details": {...} } }
HTTPKodKiedy
401invalid_keyBrakujący, nieznany albo unieważniony klucz bearer. Odpowiedź w zwykłej kopercie błędu, z nagłówkiem WWW-Authenticate: Bearer.
402insufficient_creditsKoszt maksymalny zgłoszenia przekracza dostępne saldo. Zmniejsz time_limit_seconds albo wybierz tańszy profil obliczeniowy.
403customer_suspendedKonto jest zawieszone.
404not_foundBrak takiego zadania, rozwiązania albo klucza dla tego klienta.
409last_keyOdmowa: nie można unieważnić jedynego klucza API.
410purgedDane rozwiązania są po okresie przechowywania.
413envelope_too_largeTreść żądania przekracza 20 MB.
422invalid_envelopeKoperta nie przeszła walidacji. Pola wymienia details.
422unknown_solverŻądanego solvera nie ma w katalogu.
422compute_preset_not_availableTen profil obliczeniowy nie istnieje albo żądany solver na nim nie działa.
429rate_limitedZbyt wiele żądań z tego konta. Czas oczekiwania podaje Retry-After.
Podstawowe pojęcia

Limity i kredyty

Twoje użycie ograniczają dwie rzeczy: ile obliczeń działa naraz i jak długo każde może działać.

Współbieżność
concurrency_cap (zob. GET /account) to liczba problemów rozwiązywanych jednocześnie. Powyżej niego nowe zgłoszenia czekają w kolejce.
Kredyty
Obliczenie kosztuje opłatę podstawową swojego silnika plus jego stawkę na profilu obliczeniowym, na którym działa, za sekundę. Koszt maksymalny to ta stawka przez cały time_limit_seconds i jest rezerwowany w chwili przyjęcia problemu; GET /quote podaje tę samą liczbę jeszcze przed wysłaniem. Rozliczenie obciąża faktycznie zużyte sekundy, więc uruchomienie zakończone wcześniej kosztuje mniej, a anulowane obciąża tyle, ile trwało.

Wiele obliczeń naraz? GET /events przenosi całe konto jednym połączeniem, a ?problems=a,b,c zawęża je do maksymalnie 100. Po samą odpowiedź webhook nie kosztuje ani jednego żądania.

Dokumentacja API

Problemy

Wysyłanie problemów optymalizacyjnych i śledzenie ich aż do wyniku.

POST /problems

Wysłanie jednej koperty. Zwraca identyfikator i obowiązujące limity.

envelopetreść
Wersjonowana koperta zgłoszenia (zob. Koperta).
Żądanie
curl -X POST "$BASE/problems" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @envelope.json
Odpowiedź
{
  "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, pozycja w kolejce, postęp na żywo i link do rozwiązania, gdy będzie gotowe.

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

POST /problems/{id}/wait

Utrzymanie żądania otwartego aż do statusu terminalnego. Zwraca wtedy stan zadania.

timeout_secondszapytanie
Jak długo trzymać, od 1 do 60 sekund, domyślnie 60. Po upływie czasu wraca stan taki, jaki jest, a nie błąd, więc obliczenie dłuższe niż limit wymaga powtórzenia wywołania.
Żądanie
curl -X POST "$BASE/problems/prb_9f3c.../wait?timeout_seconds=60" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{
  "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

Zdarzenia server-sent dla jednego problemu. Bez segmentu z identyfikatorem wywołanie /events obejmuje całe konto.

kindszapytanie
Dowolne z status, progress i log, po przecinku. Wszystkie trzy dla jednego problemu; strumień konta niesie status i progress, a odrzuca log.
problemszapytanie
Tylko dla /events: do 100 identyfikatorów rozdzielonych przecinkami. Bez niego całe konto.
Last-Event-IDnagłówek
Identyfikator ostatniego obsłużonego zdarzenia. Strumień wznawia się od niego, więc zerwane połączenie niczego nie gubi.
Żądanie
# 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"
Odpowiedź
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

Lista zadań albo odczyt wielu naraz przez ?ids=a,b,c.

idszapytanie
Identyfikatory rozdzielone przecinkami do zbiorczego pobrania (maksymalnie 200).
statuszapytanie
Filtrowanie po statusie cyklu życia.
tagzapytanie
Filtrowanie po tagu ustawionym w options.tags.
limit / offsetzapytanie
Okno stronicowania (limit maksymalnie 200).
Żądanie
# 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"
Odpowiedź
{
  "problems": [
    {"problem_id": "prb_a", "status": "finished", ...},
    {"problem_id": "prb_b", "status": "running", ...}
  ]
}

POST /problems/{id}/cancel

Anulowanie zadania w kolejce albo w trakcie. Zużyty czas pozostaje płatny.

Żądanie
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{"problem_id": "prb_9f3c...", "status": "cancelled"}
Dokumentacja API

Rozwiązania

Pobieranie wyników pojedynczo albo zbiorczo.

GET /problems/{id}/result

Pełna koperta rozwiązania. Jej pobranie domyka dostarczenie.

Żądanie
# 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"
Odpowiedź
{
  "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}
  }
}
Dokumentacja API

Solvery i konto

Katalog silników i stan konta.

GET /solvers

Katalog silników z cenami każdego z nich oraz wszystkie profile obliczeniowe, które może wskazać zgłoszenie.

Żądanie
curl "$BASE/solvers" -H "Authorization: Bearer $API_KEY"
Odpowiedź
{
  "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

Ile kosztowałoby zgłoszenie, według tej samej reguły, którą stosuje przyjęcie. Ten sam silnik, ten sam profil, ten sam czas, ta sama liczba.

solverzapytanie
Wymagane. Nazwa silnika lub meta-solvera.
compute_presetzapytanie
Opcjonalne. Domyślnie własny profil domyślny tego silnika.
time_limit_secondszapytanie
Opcjonalne. Domyślnie wartość platformy, ograniczona jej maksimum.
Żądanie
# 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"
Odpowiedź
{
  "solver": "highs",
  "compute_preset": "cpu-performance",
  "max_runtime_seconds": 60,
  "max_cost": 151,
  "rates": {"highs": {"base_fee": 1, "effective_rate": 2.5}}
}

max_cost to kwota, którą zgłoszenie rezerwuje na tych warunkach: najdroższy kandydat działający przez cały czas. Meta-solver zwraca jedną stawkę na każdy silnik członkowski oferujący dany profil.

GET /account

Saldo kredytów, limit równoległości, status konta.

Żądanie
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
Odpowiedź
{
  "name": "Acme Corp",
  "status": "active",
  "concurrency_cap": 4,
  "credit_balance": 4820,
  "credit_available": 4770
}
Dokumentacja API

Webhooki

Konfiguracja i podgląd wychodzących wywołań zwrotnych. Podpisy opisuje przewodnik Webhooki.

GET /account/webhook

Odczytuje twój adres zwrotny, filtry zdarzeń i pierwsze znaki sekretu podpisu. Samego sekretu nigdy nie zwracamy; jeśli go zgubiłeś, zrób rotację.

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

PUT /account/webhook

Ustawia lub czyści adres zwrotny i wybiera, które zdarzenia ma otrzymywać. Walidowany (https, osiągalny) przy zapisie.

urltreść
Endpoint https albo pusty łańcuch, żeby go wyczyścić.
Żądanie
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"]}'
Odpowiedź
{
  "url": "https://acme.example/hook",
  "events": ["terminal", "progress"],
  "secret_prefix": "whsec_<first 8>"
}

POST /account/webhook/rotate

Tworzy nowy sekret podpisu. Poprzedni podpisuje jeszcze przez 24 godziny, więc dostarczenia w drodze nadal się weryfikują.

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

POST /account/webhook/test

Wysłanie podpisanego zdarzenia próbnego na endpoint od razu.

Żądanie
curl -X POST "$BASE/account/webhook/test" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{
  "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

Ostatnie próby dostarczenia ze statusem i ostatnim błędem.

Żądanie
curl "$BASE/account/webhook/deliveries" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{
  "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

Ponowna wysyłka dostarczenia z dead-letter.

Żądanie
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
Dokumentacja API

Klucze API

Zarządzanie kluczami programowo. Pierwszy klucz tworzy się w portalu.

GET /account/keys

Lista kluczy po prefiksie. Sekret nie jest pokazywany ponownie.

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

POST /account/keys

Wydanie nowego klucza. Sekret jest zwracany dokładnie raz.

nametreść
Etykieta klucza (opcjonalna).
expires_daystreść
Liczba dni do wygaśnięcia; pominięcie oznacza brak wygaśnięcia.
Żądanie
curl -X POST "$BASE/account/keys" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "prod-2026", "expires_days": 365}'
Odpowiedź
{"id": 42, "name": "prod-2026", "prefix": "7b2e4f9d",
 "key": "<full key, shown once>"}

DELETE /account/keys/{id}

Unieważnienie klucza. Ostatni pozostały klucz jest chroniony (409).

Żądanie
curl -X DELETE "$BASE/account/keys/41" \
  -H "Authorization: Bearer $API_KEY"
Odpowiedź
{"revoked": 41}
Przewodniki

Webhooki

Daj się powiadomić. Ustaw options.webhook dla pojedynczego problemu lub domyślny endpoint na koncie, a podpisane zdarzenie otrzymasz w chwili, gdy problem się zakończy. To najtańszy sposób pracy na produkcji: nie kosztuje ani jednego własnego żądania.

Zdarzenia. Subskrypcja wybiera, co otrzymuje, przez options.webhook.events, dla pojedynczego problemu lub domyślnie na koncie. terminal, wartość domyślna, wysyła problem.updated przy statusie końcowym oraz problem.delivered następujące po pierwszym pobraniu. running wysyła każde problem.updated. progress wysyła problem.progress w trakcie rozwiązywania, każdy tik to skumulowana migawka, liczy się ostatnia. settled wysyła problem.settled, gdy znany jest koszt. Ładunki są lekkie (status, outcome, termination_status i solution_url), więc treść pobierasz jednym wywołaniem. Wiersze logu nigdy nie trafiają do webhooka: istnieją wyłącznie w strumieniu pojedynczego problemu.

Sprawdzaj każde dostarczenie. Podpisujemy według schematu Standard Webhooks, który biblioteka, którą już masz, zapewne obsłuży za ciebie. Niosą go trzy nagłówki: webhook-id, webhook-timestamp i webhook-signature, ten ostatni to HMAC-SHA256 w base64 z {id}.{timestamp}.{body} pod twoim sekretem podpisu, zapisany jako v1,.... Odrzucaj znacznik czasu starszy niż pięć minut i przyjmuj dowolną z wartości rozdzielonych spacją: rotacja podpisuje oboma sekretami przez 24 godziny, więc odbiorca, który nie pobrał jeszcze nowego, i tak znajdzie wartość, którą zweryfikuje.

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

Ponowienia. Dostarczenie jest ponawiane z rosnącym odstępem przez 24 godziny, potem zostaje oznaczone jako martwe. Przejrzyj dziennik pod GET /account/webhook/deliveries i odtwórz martwe dostarczenie przez POST /account/webhook/deliveries/{delivery_id}/retry, z portalu albo z własnego kodu; wiersze przechowujemy 90 dni. To samo zdarzenie może przyjść więcej niż raz, więc traktuj webhook-id jako klucz deduplikacji.

Przewodniki

Zarządzanie kluczami API

Pierwszy klucz tworzysz w portalu (klucz jest potrzebny, aby wywołać API). Później zarządzaj kluczami przez API, aby rotację dało się zautomatyzować.

Rotacja bez przestoju. Utwórz następcę, wdróż go, a potem unieważnij stary klucz. Unieważnienie jedynego klucza jest odrzucane, więc nie odetniesz sobie dostępu.

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}
Przewodniki

SDK w Pythonie

jumpy, nasze SDK w Pythonie

Model pisze się w Pythonie, a rozwiązanie wraca jako obiekty, z gotowym wysyłaniem, wywołaniem wait, weryfikacją webhooków i bezpiecznymi ponowieniami. Studio uruchamia je w przeglądarce, więc postać widać przed podłączeniem czegokolwiek.

Koperta jest kontraktem, więc SDK pozostaje tylko cienką warstwą wygody nad nią. Wszystko, co robi jumpy, własny klient może zrobić sam.

Więcej

Kompletne przykłady

Miks produkcji (LP) to powyższy szybki start. Poniżej ta sama koperta z decyzjami całkowitoliczbowymi: pięć projektów do wyboru, jeden budżet, wskazanie najbardziej wartościowego podzbioru.

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"
    ]
  }
}

Wysyła się to dokładnie tak jak w szybkim starcie. Odpowiedź wybiera project_3 i project_4, wydaje 29 z 30 i podaje objective_value 42 przy termination_status OPTIMAL.

Trasa dostaw (VRP) i obsada zmian nie mieszczą się w panelu z kodem, dlatego znajdują się w Studio, napisane w Pythonie i wysyłane tą samą kopertą.

Wypróbuj je w Studio

Każdy przykład wczytuje się do edytora na żywo jednym kliknięciem. Do uruchomienia piaskownicy nie potrzeba klucza.

Pierwsze obliczenie jest pięć minut stąd

Uruchom prawdziwy model na prawdziwym solverze, w przeglądarce. Bez konta, bez karty.