Parcourir la documentation

Commander par l'API

Trois appels : lire le catalogue, poser la commande, suivre le provisionnement. Le paiement tient dans la commande ou dans un appel séparé.

GET /catalog/products

Permissions : catalog:read

ParamètreTypeRequisValeurs
categorystringnonun shortname de catégorie, comparé à l'identique

Les produits privés et les catégories retirées ne sont jamais listés, et ne peuvent pas être commandés.

GET /catalog/products/{shortname}

Permissions : catalog:read

{ "data": {
  "shortname": "vps-1",
  "category": "vps",
  "title": { "fr-fr": "VPS Xeon 1", "en-us": "VPS 1" },
  "providerType": null,
  "recurrences": ["monthly", "yearly"],
  "prices": [
    { "recurrence": "monthly", "priceCents": 500 },
    { "recurrence": "yearly", "priceCents": 5500 }
  ],
  "addons": [
    { "shortname": "extra-ram-1gb",
      "title": { "fr-fr": "+1 Go RAM", "en-us": "+1 Gb RAM" },
      "kind": "RESOURCE_DELTA", "maxQuantity": 10,
      "prices": [ { "recurrence": "monthly", "priceCents": 200 } ] }
  ],
  "characteristics": [
    { "key": "os", "label": { "fr-fr": "Système", "en-us": "OS" },
      "kind": "os-picker", "required": true, "sensitive": false,
      "defaultValue": null,
      "options": ["debian-13"],
      "choices": [ { "value": "debian", "label": { "fr-fr": "Debian" }, "versions": ["13"] } ],
      "companionKeys": ["osVersion"] }
  ]
} }
ChampTypeNotes
shortnamestringà envoyer comme product dans une ligne de commande
categorystring ou nullshortname de la catégorie
titleobjet locale → textece n'est pas une chaîne
providerTypestring ou nulltype d'infrastructure, quand il est fixé
recurrencestableau de stringles cycles vendus. Ensemble ouvert : lisez-le, ne le codez pas en dur
prices[]recurrence + priceCentsprix d'un cycle, hors taxe, en centimes
addons[]voir ci-dessousoptions attachables à une ligne
characteristics[]voir ci-dessousla configuration qu'une ligne doit porter

Une option (addons[])

ChampTypeNotes
shortnamestringà envoyer dans addons[].shortname
titleobjet locale → texte
kindstringRESOURCE_DELTA, BACKUP_SLOTS, EXTRA_IPV4, FLAG
maxQuantityentierquantité maximale par ligne. 0 veut dire sans limite
prices[]recurrence + priceCentshors taxe

Une caractéristique (characteristics[])

ChampTypeNotes
keystringla clé à envoyer dans characteristics
labelobjet locale → texte
kindstringle type de saisie : text-input, select, radio-grid, os-picker, auth-credential, password-input... Ensemble ouvert
requiredbooléen
sensitivebooléenla valeur est un identifiant de connexion : stockée chiffrée, jamais renvoyée
defaultValuestring ou nulltoujours null quand sensitive vaut true
optionstableau de stringles valeurs exactes que vous pouvez envoyer. Vide pour une saisie libre
choicestableaules mêmes valeurs avec leurs libellés. Pour un os-picker, une entrée par famille, avec ses versions
companionKeystableau de stringles clés que la ligne doit porter en plus de celle-ci

Deux cas de companionKeys :

  • authValue (kind: "auth-credential") demande authType à côté, valant PASSWORD ou SSH_KEY.
  • os (kind: "os-picker") demande osVersion si vous envoyez la famille et la version séparément. Le plus simple est d'envoyer la valeur composite d'options (debian-13) et rien d'autre. C'est aussi ce que prend POST /services/{code}/reinstall.

POST /orders

Permissions : orders:write, plus celles du financement choisi. En-tête obligatoire : Idempotency-Key.

ChampTypeRequisValeursD'où il vient
linestableauoui1 à 20 lignes-
lines[].productstringoui-GET /catalog/productsshortname
lines[].recurrencestringoui quand le produit en proposeune valeur de recurrencesGET /catalog/productsrecurrences
lines[].quantityentiernon, défaut 11 à 100-
lines[].characteristicsobjet string → stringselon le produitles clés de characteristics[], valeurs prises dans optionsGET /catalog/products/{shortname}
lines[].addons[].shortnamestringoui dans une option-GET /catalog/products/{shortname}addons[]
lines[].addons[].quantityentiernon, défaut 11 à maxQuantity-
promoCodestring ou nullnon--
customerNotestring ou nullnon500 caractères au plus-
paymentobjet ou nullnon, défaut aucun paiementvoir ci-dessous-

Les valeurs de characteristics sont toujours des chaînes, même pour un nombre.

Le bloc payment

ChampTypeRequisValeurs
methodstringnon, défaut nonenone, balance, saved_method
paymentMethodIdentieroui si saved_methodGET /payment-methodsid
methodPermissions en plusEffet
none, ou pas de blocaucunela commande reste PENDING_PAYMENT. Payez-la ensuite
balancebilling.invoices:payle solde prépayé paie. Solde trop court : 402 INSUFFICIENT_BALANCE, et aucune commande n'est créée
saved_methodbilling.invoices:pay et billing.saved_methods:chargele moyen enregistré est débité, dans la limite du plafond de la clé

Un financement refusé avant toute écriture ne laisse pas de commande derrière lui : moyen introuvable, moyen inactif, solde court, plafond dépassé.

Un total nul, après une remise de 100 %, se règle sans paiement et la commande passe directement à PAID.

Exemple

curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: 6f1c0b62-4f4b-4f8e-9b06-0c1d7a5c9e21" \
     -d '{
       "lines": [{
         "product": "vps-1",
         "recurrence": "monthly",
         "quantity": 1,
         "characteristics": {
           "os": "debian-13",
           "location": "paris",
           "authType": "SSH_KEY",
           "authValue": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... admin@laptop"
         },
         "addons": [{ "shortname": "extra-ram-1gb", "quantity": 2 }]
       }],
       "customerNote": "Migration du serveur web",
       "payment": { "method": "balance" }
     }' \
     https://api.freshperf.fr/v1/orders
{ "data": {
  "order": {
    "orderNumber": "ORD-20260828-0001",
    "status": "PAID",
    "currency": "EUR",
    "subtotalCents": 900, "taxCents": 180, "discountCents": 0, "totalCents": 1080,
    "amountPaidCents": 1080, "remainingDueCents": 0,
    "promoCode": null, "customerNote": "Migration du serveur web",
    "lines": [ { "productShortname": "vps-1", "recurrence": "monthly", "quantity": 1,
                 "unitPriceCents": 500, "unitTaxCents": 100, "totalCents": 1080,
                 "addons": [ { "shortname": "extra-ram-1gb", "quantity": 2, "unitPriceCents": 200 } ],
                 "configuration": { "os": "debian-13", "location": "paris" } } ],
    "serviceCodes": ["SRV-TKH243-7763"],
    "createdAt": 1787907301139, "paidAt": 1787907315110
  },
  "promoError": null,
  "payment": { "status": "CAPTURED", "paymentId": 244, "amountCents": 1080,
               "invoiceCode": "INV-202608-0084", "balanceCents": 3320 }
} }
ChampTypeNotes
order.statusstringDRAFT, PENDING_PAYMENT, AUTHORIZED, PAID, FULFILLED, CANCELLED, FAILED, REFUNDED
order.serviceCodestableau de stringles services que la commande a créés. Vide tant que le paiement n'est pas réglé et le provisionnement pas fini
order.lines[].configurationobjet string → stringla configuration retenue. Les valeurs sensitive n'y sont jamais
promoErrorstring ou nullrenseigné quand le code promo n'a pas pu s'appliquer. La commande tient au plein tarif
payment.statusstringNONE quand rien n'a été débité, FREE sur un total nul, sinon une valeur de Payment.Status

Poser une commande par l'API ne touche jamais le panier ouvert dans le tableau de bord.

Refus

ErreurStatutQuand
NO_ITEMS400lines vide ou absent
TOO_MANY_ITEMS400plus de 20 lignes
PRODUCT_NOT_FOUND:<shortname>400produit inconnu ou privé
PRODUCT_NOT_ORDERABLE:<shortname>400catégorie retirée de la vente
RECURRENCE_REQUIRED:<shortname>400le produit se vend par cycles et recurrence manque
RECURRENCE_NOT_OFFERED:<shortname>400ce cycle n'existe pas pour ce produit
MISSING_REQUIRED:<clé>400une caractéristique obligatoire manque
INVALID_METADATA_KEY:<clé>400une clé que le produit ne déclare pas
INVALID_METADATA_VALUE:<clé>400une valeur hors options
ADDON_NOT_AVAILABLE:<shortname>400option non proposée par ce produit
ADDON_MAX_QUANTITY:<shortname>400quantité au-delà de maxQuantity
VALIDATION_ERROR:lines.quantity400quantité hors de 1 à 100
VALIDATION_ERROR:customerNote400au-delà de 500 caractères
VALIDATION_ERROR:payment.method400method hors liste
PAYMENT_METHOD_ID_REQUIRED400saved_method sans paymentMethodId
PAYMENT_METHOD_NOT_FOUND404moyen inconnu du compte
PAYMENT_METHOD_NOT_ACTIVE400moyen expiré ou bloqué
INSUFFICIENT_BALANCE402solde trop court
PAYMENT_DENIED402la banque a refusé. La commande reste payable autrement
API_KEY_SPENDING_CAP_EXCEEDED403le plafond de la clé ne couvre pas le total
PAYMENT_IN_PROGRESS409un autre paiement de la même commande tourne
PAYMENT_RECORDED_MISMATCH409anomalie de rapprochement. Contactez le support
ORDER_CONCURRENT_UPDATE409la commande a changé pendant l'écriture. Réessayez

POST /orders/{orderNumber}/pay

Permissions : billing.invoices:pay, plus billing.saved_methods:charge avec saved_method. En-tête obligatoire : Idempotency-Key.

Paie ce qui reste dû sur une commande laissée en attente.

ChampTypeRequisValeursD'où il vient
methodstringouibalance, saved_method-
paymentMethodIdentieroui si saved_method> 0GET /payment-methodsid
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: pay-ORD-20260828-0001" \
     -d '{"method":"saved_method","paymentMethodId":14}' \
     https://api.freshperf.fr/v1/orders/ORD-20260828-0001/pay

Répond le même bloc payment que POST /orders.

ErreurStatutQuand
ORDER_NOT_FOUND404numéro inconnu
ORDER_NOT_PAYABLE, ORDER_ALREADY_SETTLED, ORDER_ALREADY_PAID400il n'y a rien à payer
AMOUNT_INVALID400le reste dû n'est pas un montant valable

Plus les refus de paiement de la table précédente.

GET /orders et GET /orders/{orderNumber}

Permissions : orders:read

ParamètreTypeRequisValeurs
limitentiernon, défaut 201 à 100
cursorstringnonpagination.nextCursor de la page précédente

La liste rend un résumé (orderNumber, status, currency, totalCents, amountPaidCents, remainingDueCents, createdAt, paidAt) et omet les brouillons. Le détail rend la commande entière, avec lines[] et serviceCodes.

Suivre le provisionnement

  1. GET /orders/{orderNumber} : status passe à PAID, puis à FULFILLED une fois chaque service en place.
  2. serviceCodes se remplit avec les codes créés. C'est le lien entre une commande et les services qu'elle a produits.
  3. GET /services/{code} rend provisioned: false tant que l'infrastructure se met en place, puis true.

Le provisionnement tourne en arrière-plan et prend quelques minutes. Interrogez toutes les 10 à 30 secondes.