Se rendre au contenu

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.

  1. POST /problems
  2. queued le problème a un id
  3. POST /problems/{id}/wait
  4. running c'est ici que l'appel wait patiente
  5. finished le wait rend la main, ou votre webhook se déclenche
  6. GET /problems/{id}/result
  7. solution vous avez la réponse
Démarrer

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.

Démarrer

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.

  1. 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
  2. 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
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"
    ]
  }
}

Prêt pour les détails ? Référence d'API complète

Démarrer

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.

Concepts de base

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.

Concepts de base

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_key et tags. 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 /solvers liste tous les profils et les moteurs qui les proposent.
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"
    ]
  }
}

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.

Concepts de base

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 name composé de lettres, de chiffres et de tirets bas, ne commençant pas par un chiffre. type vaut continuous (par défaut), integer ou binary. lower et upper sont 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 un name optionnel qui revient dans le diagnostic. Un expr, ce sont des termes à gauche, un nombre à droite, joints par <=, >= ou ==.
objective
sense vaut min ou max ; expr reprend 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.

Concepts de base

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.

StatutSignification
pending_inputAccepté, en attente du modèle que vous envoyez séparément.
queuedAccepté et en attente de résolution.
runningUn moteur l'a prise en charge et la résout.
finishedUne réponse existe (voir outcome). La solution est prête à être récupérée.
failedLe 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).
cancelledAnnulé par vous, ou expiré en file.
RésultatSignification
solvedLa réponse demandée : une solution optimale ou dans la tolérance. result contient le point, objective_value est présent.
no_solutionProuvé : aucune réponse n'existe telle que demandée (infaisable ou non borné). termination_status porte le diagnostic exact.
limitUn 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.
errorLe 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.

Concepts de base

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.

Concepts de base

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": {...} } }
HTTPCodeQuand
401invalid_keyClé porteuse manquante, inconnue ou révoquée. Répondue dans l'enveloppe d'erreur habituelle, avec un en-tête WWW-Authenticate: Bearer.
402insufficient_creditsLe coût maximum de la soumission dépasse le solde disponible. Baissez time_limit_seconds, ou choisissez un profil de calcul moins cher.
403customer_suspendedLe compte est suspendu.
404not_foundAucun problème, aucune solution ni aucune clé de ce type pour ce client.
409last_keyRefusé : vous ne pouvez pas révoquer votre unique clé API.
410purgedLa charge utile de la solution a dépassé sa durée de conservation.
413envelope_too_largeLe corps de la requête dépasse 20 Mo.
422invalid_envelopeL'enveloppe a échoué à la validation. details liste les champs.
422unknown_solverLe solveur demandé n'est pas au catalogue.
422compute_preset_not_availableCe profil de calcul n'existe pas, ou le solveur demandé n'y tourne pas.
429rate_limitedTrop de requêtes pour ce compte. Retry-After indique combien de temps attendre.
Concepts de base

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 (voir GET /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 /quote donne 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.

Référence d'API

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).
Requête
curl -X POST "$BASE/problems" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d @envelope.json
Réponse
{
  "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.

Requête
curl "$BASE/problems/prb_9f3c..." \
  -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.
Requête
curl -X POST "$BASE/problems/prb_9f3c.../wait?timeout_seconds=60" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.
Requête
# 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"
Réponse
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).
Requête
# 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"
Réponse
{
  "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.

Requête
curl -X POST "$BASE/problems/prb_9f3c.../cancel" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{"problem_id": "prb_9f3c...", "status": "cancelled"}
Référence d'API

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.

Requête
# 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"
Réponse
{
  "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}
  }
}
Référence d'API

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.

Requête
curl "$BASE/solvers" -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.
Requête
# 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"
Réponse
{
  "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.

Requête
curl "$BASE/account" -H "Authorization: Bearer $API_KEY"
Réponse
{
  "name": "Acme Corp",
  "status": "active",
  "concurrency_cap": 4,
  "credit_balance": 4820,
  "credit_available": 4770
}
Référence d'API

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.

Requête
curl "$BASE/account/webhook" -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.
Requête
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"]}'
Réponse
{
  "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.

Requête
curl -X POST "$BASE/account/webhook/rotate" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.

Requête
curl -X POST "$BASE/account/webhook/test" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.

Requête
curl "$BASE/account/webhook/deliveries" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.

Requête
curl -X POST "$BASE/account/webhook/deliveries/dlv_4c1e.../retry" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{"id": "dlv_4c1e...", "status": "pending", "attempts": [...]}
Référence d'API

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é.

Requête
curl "$BASE/account/keys" -H "Authorization: Bearer $API_KEY"
Réponse
{
  "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.
Requête
curl -X POST "$BASE/account/keys" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "prod-2026", "expires_days": 365}'
Réponse
{"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).

Requête
curl -X DELETE "$BASE/account/keys/41" \
  -H "Authorization: Bearer $API_KEY"
Réponse
{"revoked": 41}
Guides

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.

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

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.

Guides

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.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}
Guides

SDK Python

jumpy, notre 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.

Plus

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.

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

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.