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.
-
POST
/problems -
queuedproblem ma identyfikator -
POST
/problems/{id}/wait -
runningtutaj wywołanie wait pozostaje otwarte -
finishedwait zwraca odpowiedź albo uruchamia się twój webhook -
GET
/problems/{id}/result -
solutionmasz odpowiedź
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.
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.
-
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 -
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
{
"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
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.
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.
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_keyitags. 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 /solverswymienia wszystkie profile i silniki, które je oferują.
{
"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.
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
namez liter, cyfr i podkreśleń, niezaczynającej się od cyfry.typetocontinuous(domyślnie),integeralbobinary.loweriupperto 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.exprto składniki po lewej, jedna liczba po prawej, połączone przez<=,>=albo==. - objective
senseprzyjmuje wartośćminalbomax;exprkorzysta 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.
Status i wynik
Dwa zamrożone słowniki. status to cykl życia problemu; gdy osiągnie finished, outcome klasyfikuje odpowiedź.
| Status | Znaczenie |
|---|---|
| pending_input | Przyjęte, wciąż czeka na model, który wysyłasz osobno. |
| queued | Przyjęte i czeka na rozwiązanie. |
| running | Silnik podjął zadanie i je rozwiązuje. |
| finished | Odpowiedź istnieje (zob. outcome). Rozwiązanie jest gotowe do pobrania. |
| failed | Usł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). |
| cancelled | Anulowane przez klienta albo wygasłe w kolejce. |
| Wynik | Znaczenie |
|---|---|
| solved | Odpowiedź, o którą pytano: rozwiązanie optymalne albo mieszczące się w tolerancji. W result znajduje się punkt, objective_value jest obecne. |
| no_solution | Dowiedzione: dla tak postawionego pytania odpowiedź nie istnieje (sprzeczność albo brak ograniczenia). Dokładną diagnozę niesie termination_status. |
| limit | Obliczenie zatrzymał limit (czas, pamięć, iteracje). Jeśli istnieje bieżące najlepsze rozwiązanie, znajduje się w result, a objective_value jest obecne. |
| error | Solver 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.
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.
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": {...} } }
| HTTP | Kod | Kiedy |
|---|---|---|
| 401 | invalid_key | Brakujący, nieznany albo unieważniony klucz bearer. Odpowiedź w zwykłej kopercie błędu, z nagłówkiem WWW-Authenticate: Bearer. |
| 402 | insufficient_credits | Koszt maksymalny zgłoszenia przekracza dostępne saldo. Zmniejsz time_limit_seconds albo wybierz tańszy profil obliczeniowy. |
| 403 | customer_suspended | Konto jest zawieszone. |
| 404 | not_found | Brak takiego zadania, rozwiązania albo klucza dla tego klienta. |
| 409 | last_key | Odmowa: nie można unieważnić jedynego klucza API. |
| 410 | purged | Dane rozwiązania są po okresie przechowywania. |
| 413 | envelope_too_large | Treść żądania przekracza 20 MB. |
| 422 | invalid_envelope | Koperta nie przeszła walidacji. Pola wymienia details. |
| 422 | unknown_solver | Żądanego solvera nie ma w katalogu. |
| 422 | compute_preset_not_available | Ten profil obliczeniowy nie istnieje albo żądany solver na nim nie działa. |
| 429 | rate_limited | Zbyt wiele żądań z tego konta. Czas oczekiwania podaje Retry-After. |
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_secondsi jest rezerwowany w chwili przyjęcia problemu;GET /quotepodaje 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.
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).
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, pozycja w kolejce, postęp na żywo i link do rozwiązania, gdy będzie gotowe.
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
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.
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
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.
# 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
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).
# 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
Anulowanie zadania w kolejce albo w trakcie. Zużyty czas pozostaje płatny.
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
-H "Authorization: Bearer $API_KEY"
{"problem_id": "prb_9f3c...", "status": "cancelled"}
Rozwiązania
Pobieranie wyników pojedynczo albo zbiorczo.
GET
/problems/{id}/result
Pełna koperta rozwiązania. Jej pobranie domyka dostarczenie.
# 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}
}
}
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.
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
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.
# 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 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.
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
{
"name": "Acme Corp",
"status": "active",
"concurrency_cap": 4,
"credit_balance": 4820,
"credit_available": 4770
}
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ę.
curl "$BASE/account/webhook" -H "Authorization: Bearer $API_KEY"
{
"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ć.
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
Tworzy nowy sekret podpisu. Poprzedni podpisuje jeszcze przez 24 godziny, więc dostarczenia w drodze nadal się weryfikują.
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
Wysłanie podpisanego zdarzenia próbnego na endpoint od razu.
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
Ostatnie próby dostarczenia ze statusem i ostatnim błędem.
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
Ponowna wysyłka dostarczenia z dead-letter.
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
-H "Authorization: Bearer $API_KEY"
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
Klucze API
Zarządzanie kluczami programowo. Pierwszy klucz tworzy się w portalu.
GET
/account/keys
Lista kluczy po prefiksie. Sekret nie jest pokazywany ponownie.
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
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.
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}
Unieważnienie klucza. Ostatni pozostały klucz jest chroniony (409).
curl -X DELETE "$BASE/account/keys/41" \
-H "Authorization: Bearer $API_KEY"
{"revoked": 41}
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.
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.
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 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}
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.
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.
{
"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.