L'API NexOR Optimization
Soumettez un problème, obtenez une solution. Un appel REST en entrée, une réponse en sortie, stable sous /solve/v1 et authentifié par une clé porteuse. Chaque point de terminaison de cette page fonctionne aujourd'hui ; le SDK Python est un aperçu et est signalé comme tel.
Formule gratuite, sans carte. Votre première résolution prend quelques minutes.
-
POST
/problems -
queuedle problème a un id -
POST
/problems/{id}/wait -
runningc'est ici que l'appel wait patiente -
finishedle wait rend la main, ou votre webhook se déclenche -
GET
/problems/{id}/result -
solutionvous avez la réponse
Un solveur que vous appelez en HTTP
Vous envoyez un problème d'optimisation mathématique en JSON. Nous l'exécutons sur nos solveurs et vous rendons la solution. C'est indépendant du domaine : programmes linéaires, modèles mixtes en nombres entiers, tournées, ordonnancement, tout ce que vous savez exprimer voyage dans la même enveloppe.
Chaque intégration repose sur les trois mêmes gestes : soumettre un problème et obtenir un id, attendre sur une seule requête ou laisser un webhook signé vous parvenir, puis récupérer la solution. La section suivante fait les trois en moins d'une minute.
Nous traitons le corps de votre problème comme opaque. Le manager valide l'enveloppe et mesure le calcul, mais ne lit jamais votre modèle. C'est ce qui garde une seule API générique pour toutes les classes de problèmes.
Votre première résolution, en trois étapes
De zéro à une vraie réponse en quelques minutes. Un mix de production à deux produits : maximiser la marge sous des limites d'heures machine et de matières. L'optimum est de 30 chaises et 5 tables, objectif 1750.
Essayez-le en direct d'abord
Sans compte, sans clé, gratuit. Écrivez un modèle en Python et exécutez-le contre le vrai solveur, dans votre navigateur.
-
Copier une clé API
Donnez-lui un nom, choisissez une expiration, c'est tout le formulaire. Cette clé vous identifie à chaque appel API que vous faites. Le secret ne s'affiche qu'une fois, copiez-le tout de suite. C'est la seule étape qui se passe dans le navigateur.
Ouvrir les clés API -
Soumettre, attendre, récupérer
Collez votre clé dans l'exemple ci-dessous et exécutez-le : soumettez l'enveloppe, gardez une requête ouverte jusqu'à la fin du problème, puis récupérez la solution. C'est le chemin le plus court depuis un portable, où rien ne peut recevoir un webhook. Dès que vous avez un serveur, enregistrez un webhook et supprimez l'étape du milieu.
# 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, ...}
Le fichier envelope.json utilisé ci-dessus
{
"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"
]
}
}
Prêt pour les détails ? Référence d'API complète
Clés porteuses
Chaque requête porte votre clé API comme jeton porteur. Les clés sont cantonnées à l'API d'optimisation : une clé qui fuit ne peut toucher à rien d'autre dans votre compte.
Authorization: Bearer <your-api-key>
Créez votre première clé dans le portail. Ensuite, vous pouvez lister, créer et révoquer les clés via l'API elle-même (voir Clés API). Gardez vos clés côté serveur et faites-en la rotation sans interruption quand c'est nécessaire.
Le parcours d'une résolution
La résolution est asynchrone. La soumission renvoie immédiatement un identifiant et un statut queued ; la résolution tourne sur notre infrastructure ; le résultat vous parvient.
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
Trois façons de savoir que c'est terminé. Un webhook vous parvient dès que le problème se termine et ne vous coûte aucune requête : c'est ce qu'un serveur doit utiliser. POST /problems/{id}/wait garde une requête ouverte jusque-là, c'est ce qu'un script sur un portable doit utiliser. GET /problems/{id}/events diffuse les mêmes événements sur une seule connexion, et porte la progression du solveur pendant qu'il travaille. Chaque payload est léger, vous récupérez donc la solution en un seul appel.
L'enveloppe de soumission
Une enveloppe versionnée autour de quatre parties. Nous possédons et validons l'enveloppe ; le problem à l'intérieur vous appartient.
- api_version
- La version du protocole. Toujours
"1"aujourd'hui. - problem
- Votre modèle : variables, contraintes, un objectif. Opaque pour nous, validé par le solveur.
- solver
- Le moteur à exécuter : un nom de solveur concret, ou un nom de méta-solveur, c'est-à-dire un ensemble nommé de moteurs membres dont n'importe lequel peut prendre le travail. Le réglage du solveur voyage à l'intérieur de
problem. - options
time_limit_seconds,compute_preset,webhook,idempotency_keyettags. Tous facultatifs.- options.compute_preset
- Le calcul que la résolution réserve, par code (
cpu-standard,gpu-standard). Omettez-le et le moteur tourne sur son profil par défaut.GET /solversliste tous les profils et les moteurs qui les proposent.
{
"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"
]
}
}
Schémas JSON lisibles par machine : admission_request_v1.json
Le schéma type problem comme un objet opaque, parce que le manager ne le lit jamais. La section ci-dessous décrit la forme que les solveurs, eux, lisent.
Décrire un modèle
Trois clés dans problem : les variables que vous décidez, les contraintes qu'elles doivent respecter, et un objectif. Les expressions sont des chaînes, et elles sont linéaires.
- variables
- Une liste. Chaque entrée a besoin d'un
namecomposé de lettres, de chiffres et de tirets bas, ne commençant pas par un chiffre.typevautcontinuous(par défaut),integeroubinary.loweretuppersont des bornes numériques optionnelles ; omettez-en une et la variable est non bornée de ce côté. - constraints
- Une liste de chaînes
expr, chacune avec unnameoptionnel qui revient dans le diagnostic. Unexpr, ce sont des termes à gauche, un nombre à droite, joints par<=,>=ou==. - objective
sensevautminoumax;exprreprend les mêmes termes, sans comparaison ni constante. Une constante ne fait que décaler la valeur : elle est refusée plutôt que supprimée en silence.
Termes. Un terme est un coefficient, un astérisque et un nom de variable : 4*chairs. Un coefficient égal à un peut s'écrire par le nom seul. Les termes se joignent par un plus ou un moins entouré d'espaces : 4*chairs + 8*tables - 2*offcuts. Tout ce qui n'est pas linéaire est refusé : pas de produit de deux variables, pas de fonctions, pas d'exposants.
Déclarer une variable integer ou binary, c'est ce qui transforme un programme linéaire en programme mixte en nombres entiers. Rien d'autre ne change dans l'enveloppe, et le prix reste les frais de base plus les secondes. Un exemple commenté complet, à cinq variables binaires, figure ci-dessous.
Statut et résultat
Deux vocabulaires figés. status est le cycle de vie du problème ; une fois qu'il atteint finished, outcome qualifie la réponse.
| Statut | Signification |
|---|---|
| pending_input | Accepté, en attente du modèle que vous envoyez séparément. |
| queued | Accepté et en attente de résolution. |
| running | Un moteur l'a prise en charge et la résout. |
| finished | Une réponse existe (voir outcome). La solution est prête à être récupérée. |
| failed | Le service n'a pas pu exécuter la résolution ; error_code en donne la raison. Un modèle que le solveur choisi ne sait pas traiter passe plutôt en finished (outcome error). |
| cancelled | Annulé par vous, ou expiré en file. |
| Résultat | Signification |
|---|---|
| solved | La réponse demandée : une solution optimale ou dans la tolérance. result contient le point, objective_value est présent. |
| no_solution | Prouvé : aucune réponse n'existe telle que demandée (infaisable ou non borné). termination_status porte le diagnostic exact. |
| limit | Un budget a arrêté la résolution en premier (temps, mémoire, itérations). S'il existe une solution courante, result la contient et objective_value est présent. |
| error | Le solveur a tourné et a cassé sur ce modèle : échec numérique, modèle invalide, ou une classe de contrainte que ce moteur ne sait pas exprimer. Cela reste une réponse sur ce modèle. Pas de réessai. Choisissez un autre solveur ou reformulez. |
Branchez votre logique sur ces deux champs uniquement. La solution porte aussi, tel quel, le termination_status MOI du solveur (comme OPTIMAL ou TIME_LIMIT) comme détail de diagnostic. Traitez-le comme du texte d'affichage, pas comme un contrat.
Idempotence
Définissez options.idempotency_key sur une chaîne unique. Si un incident réseau vous fait réessayer, la seconde soumission renvoie le problème d'origine au lieu de créer un doublon (et une provision de crédits en double).
"options": { "idempotency_key": "order-4821-solve", "time_limit_seconds": 5 }
Les clés sont limitées à votre compte. En réutiliser une renvoie toujours le premier problème créé avec elle.
Erreurs
Les échecs reviennent avec le statut HTTP correspondant et un corps JSON. Branchez-vous sur code, affichez message et lisez details quand un champ est en cause.
{ "error": { "code": "invalid_envelope", "message": "...", "details": {...} } }
| HTTP | Code | Quand |
|---|---|---|
| 401 | invalid_key | Clé porteuse manquante, inconnue ou révoquée. Répondue dans l'enveloppe d'erreur habituelle, avec un en-tête WWW-Authenticate: Bearer. |
| 402 | insufficient_credits | Le coût maximum de la soumission dépasse le solde disponible. Baissez time_limit_seconds, ou choisissez un profil de calcul moins cher. |
| 403 | customer_suspended | Le compte est suspendu. |
| 404 | not_found | Aucun problème, aucune solution ni aucune clé de ce type pour ce client. |
| 409 | last_key | Refusé : vous ne pouvez pas révoquer votre unique clé API. |
| 410 | purged | La charge utile de la solution a dépassé sa durée de conservation. |
| 413 | envelope_too_large | Le corps de la requête dépasse 20 Mo. |
| 422 | invalid_envelope | L'enveloppe a échoué à la validation. details liste les champs. |
| 422 | unknown_solver | Le solveur demandé n'est pas au catalogue. |
| 422 | compute_preset_not_available | Ce profil de calcul n'existe pas, ou le solveur demandé n'y tourne pas. |
| 429 | rate_limited | Trop de requêtes pour ce compte. Retry-After indique combien de temps attendre. |
Limites et crédits
Deux choses bornent votre usage : combien de résolutions tournent en même temps, et combien de temps chacune peut tourner.
- Concurrence
- Votre
concurrency_cap(voirGET /account) est le nombre de problèmes résolus en même temps. Au-delà, les nouvelles soumissions attendent dans la file. - Crédits
- Une résolution coûte les frais de base de son moteur plus son tarif sur le profil de calcul où elle tourne, à la seconde. Le coût maximum, c'est ce tarif sur la totalité de
time_limit_seconds, et il est réservé quand le problème est admis ;GET /quotedonne le même chiffre avant que vous ne soumettiez. Le règlement facture les secondes réellement utilisées : une exécution qui se termine plus tôt coûte moins, et une exécution annulée facture ce qu'elle a tourné.
Beaucoup de résolutions à la fois ? GET /events porte tout le compte sur une seule connexion, et ?problems=a,b,c la restreint à 100 au maximum. Pour la réponse elle-même, un webhook ne vous coûte aucune requête.
Problèmes
Soumettez des problèmes d'optimisation et suivez-les jusqu'au résultat.
POST
/problems
Soumettez une enveloppe. Renvoie l'identifiant et les limites effectives.
- envelopecorps
- Une enveloppe de soumission versionnée (voir L'enveloppe).
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}
Statut, position dans la file, progression en direct, et le lien vers la solution quand elle est prête.
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
Gardez la requête ouverte jusqu'à un statut terminal. Renvoie l'état du problème.
- timeout_secondsrequête
- Durée de maintien, de 1 à 60 secondes, 60 par défaut. À l'expiration vous recevez l'état tel quel, pas une erreur : une résolution plus longue que le plafond demande de rappeler.
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
Événements server-sent pour un problème. Retirez le segment d'identifiant et appelez /events pour tout le compte.
- kindsrequête
- Au choix status, progress et log, séparés par des virgules. Les trois sur un problème ; le flux du compte porte status et progress, et refuse log.
- problemsrequête
- Sur /events uniquement : jusqu'à 100 identifiants séparés par des virgules. Tout le compte si vous l'omettez.
- Last-Event-IDen-tête
- L'identifiant du dernier événement traité. Le flux reprend à partir de là : une connexion coupée ne perd rien.
# 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
Listez vos problèmes, ou lisez-en plusieurs d'un coup avec ?ids=a,b,c.
- idsrequête
- Identifiants séparés par des virgules pour une récupération multiple directe (200 au maximum).
- statusrequête
- Filtrer par statut du cycle de vie.
- tagrequête
- Filtrer par une étiquette définie dans options.tags.
- limit / offsetrequête
- Fenêtre de pagination (limite de 200 au maximum).
# 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
Annulez un problème en file ou en cours. Le temps déjà consommé reste facturable.
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
-H "Authorization: Bearer $API_KEY"
{"problem_id": "prb_9f3c...", "status": "cancelled"}
Solutions
Récupérez les résultats, un par un ou en lot.
GET
/problems/{id}/result
L'enveloppe de solution complète. La récupérer termine la livraison.
# 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}
}
}
Solveurs et compte
Le catalogue des moteurs et l'état de votre compte.
GET
/solvers
Le catalogue des moteurs avec les prix de chacun, et tous les profils de calcul qu'une soumission peut nommer.
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
Ce que coûterait une soumission, selon la règle qu'applique l'admission. Même moteur, même profil, même durée, même chiffre.
- solverrequête
- Obligatoire. Un nom de moteur ou de méta-solveur.
- compute_presetrequête
- Facultatif. Par défaut, le profil par défaut de ce moteur.
- time_limit_secondsrequête
- Facultatif. Par défaut la valeur de la plateforme, plafonnée à son maximum.
# what a 60 second solve on HiGHS would cost, on the larger CPU preset
curl "$BASE/quote?solver=highs&compute_preset=cpu-performance&time_limit_seconds=60" \
-H "Authorization: Bearer $API_KEY"
{
"solver": "highs",
"compute_preset": "cpu-performance",
"max_runtime_seconds": 60,
"max_cost": 151,
"rates": {"highs": {"base_fee": 1, "effective_rate": 2.5}}
}
max_cost est ce qu'une soumission réserve à ces conditions : le candidat le plus cher tournant pendant toute la durée. Un méta-solveur répond un tarif par moteur membre qui propose le profil.
GET
/account
Solde de crédits, plafond de concurrence, statut du compte.
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
{
"name": "Acme Corp",
"status": "active",
"concurrency_cap": 4,
"credit_balance": 4820,
"credit_available": 4770
}
Webhooks
Configurez et inspectez vos rappels sortants. Voir le guide Webhooks pour les signatures.
GET
/account/webhook
Lit votre URL de rappel, vos filtres d'événements et les premiers caractères du secret de signature. Le secret lui-même n'est jamais renvoyé ; faites une rotation si vous l'avez perdu.
curl "$BASE/account/webhook" -H "Authorization: Bearer $API_KEY"
{
"url": "https://acme.example/hook",
"events": ["terminal"],
"secret_prefix": "whsec_<first 8>"
}
PUT
/account/webhook
Définit ou efface l'URL de rappel, et choisit les événements qu'elle reçoit. Validée (https, joignable) à l'enregistrement.
- urlcorps
- Le point de terminaison https, ou une chaîne vide pour l'effacer.
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
Génère un nouveau secret de signature. Le précédent continue de signer pendant 24 heures, pour que les livraisons en cours restent vérifiables.
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
Envoyez tout de suite un événement d'exemple signé vers votre point de terminaison.
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
Tentatives de livraison récentes avec leur statut et la dernière erreur.
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
Renvoyer une livraison mise en dead-letter.
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
-H "Authorization: Bearer $API_KEY"
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
Clés API
Gérez les clés par programme. La première clé se crée dans le portail.
GET
/account/keys
Listez vos clés par préfixe. Le secret n'est plus jamais affiché.
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
Générez une nouvelle clé. Le secret est renvoyé une seule fois.
- namecorps
- Un libellé pour la clé (optionnel).
- expires_dayscorps
- Nombre de jours avant expiration, ou omettez pour aucune.
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}
Révoquer une clé. Votre dernière clé restante est protégée (409).
curl -X DELETE "$BASE/account/keys/41" \
-H "Authorization: Bearer $API_KEY"
{"revoked": 41}
Webhooks
Laissez-nous vous appeler. Définissez options.webhook par problème, ou un endpoint par défaut sur votre compte, et recevez un événement signé dès qu'un problème se termine. C'est la façon la moins chère de tourner en production : elle ne vous coûte aucune requête.
Événements. Un abonnement choisit ce qu'il reçoit via options.webhook.events, par problème ou sur le réglage par défaut du compte. terminal, la valeur par défaut, envoie problem.updated sur un statut terminal ainsi que le problem.delivered qui suit le premier téléchargement. running envoie chaque problem.updated. progress envoie problem.progress pendant la résolution, chaque tick étant un instantané cumulatif où le dernier l'emporte. settled envoie problem.settled, une fois le coût connu. Les charges utiles sont légères (status, outcome, termination_status et un solution_url) : récupérez le corps en un appel. Les lignes de log ne sont jamais livrées à un webhook, elles n'existent que sur le flux d'un problème.
Vérifiez chaque livraison. Nous signons selon le schéma Standard Webhooks, qu'une bibliothèque déjà présente chez vous sait sans doute traiter. Trois en-têtes le portent : webhook-id, webhook-timestamp et webhook-signature, ce dernier un HMAC-SHA256 en base64 sur {id}.{timestamp}.{body} avec votre secret de signature, écrit v1,.... Refusez un horodatage vieux de plus de cinq minutes, et acceptez n'importe laquelle des valeurs séparées par une espace : une rotation signe avec les deux secrets pendant 24 heures, si bien qu'un destinataire qui n'a pas encore pris le nouveau trouve malgré tout une valeur vérifiable.
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
Réessais. Une livraison est réessayée avec temporisation croissante pendant 24 heures, puis marquée morte. Consultez le journal sur GET /account/webhook/deliveries et rejouez une livraison morte avec POST /account/webhook/deliveries/{delivery_id}/retry, depuis le portail ou votre propre code ; les lignes sont conservées 90 jours. Un même événement peut arriver plusieurs fois : utilisez webhook-id comme clé de déduplication.
Gérer les clés API
Votre première clé se crée dans le portail (il faut une clé pour appeler l'API). Ensuite, gérez-les via l'API pour automatiser la rotation.
Rotation sans interruption. Créez la nouvelle clé, déployez-la, puis révoquez l'ancienne. Révoquer votre unique clé est refusé : impossible de vous enfermer dehors.
# 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 Python
Écrivez le modèle en Python et récupérez la solution sous forme d'objets, avec la soumission, l'appel wait, la vérification des webhooks et les réessais idempotents déjà emballés. Le Studio l'exécute dans le navigateur, de quoi en lire la forme avant de câbler quoi que ce soit.
L'enveloppe est le contrat, le SDK reste donc une fine couche de confort par-dessus. Tout ce que jumpy fait, votre propre client peut le faire.
Exemples commentés
Un mix de production (LP) est le démarrage rapide ci-dessus. Voici la même enveloppe avec des décisions entières : cinq projets candidats, un budget, choisir le sous-ensemble qui vaut le plus.
{
"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"
]
}
}
Postez-la exactement comme le démarrage rapide. La réponse choisit project_3 et project_4, dépense 29 des 30, et indique un objective_value de 42 avec un termination_status OPTIMAL.
Une tournée de livraison (VRP) et une couverture d'équipes dépassent la taille d'un panneau de code : elles vivent dans le Studio, écrites en Python et soumises par la même enveloppe.
Essayez-les dans le Studio
Chaque exemple se charge dans l'éditeur en direct en un clic. Aucune clé requise pour lancer la sandbox.
Votre première résolution est à cinq minutes
Lancez un vrai modèle sur un vrai solveur, dans le navigateur. Sans compte, sans carte bancaire.