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ètre | Type | Requis | Valeurs |
|---|---|---|---|
category | string | non | un 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"] } ] } }
| Champ | Type | Notes |
|---|---|---|
shortname | string | à envoyer comme product dans une ligne de commande |
category | string ou null | shortname de la catégorie |
title | objet locale → texte | ce n'est pas une chaîne |
providerType | string ou null | type d'infrastructure, quand il est fixé |
recurrences | tableau de string | les cycles vendus. Ensemble ouvert : lisez-le, ne le codez pas en dur |
prices[] | recurrence + priceCents | prix d'un cycle, hors taxe, en centimes |
addons[] | voir ci-dessous | options attachables à une ligne |
characteristics[] | voir ci-dessous | la configuration qu'une ligne doit porter |
Une option (addons[])
| Champ | Type | Notes |
|---|---|---|
shortname | string | à envoyer dans addons[].shortname |
title | objet locale → texte | |
kind | string | RESOURCE_DELTA, BACKUP_SLOTS, EXTRA_IPV4, FLAG |
maxQuantity | entier | quantité maximale par ligne. 0 veut dire sans limite |
prices[] | recurrence + priceCents | hors taxe |
Une caractéristique (characteristics[])
| Champ | Type | Notes |
|---|---|---|
key | string | la clé à envoyer dans characteristics |
label | objet locale → texte | |
kind | string | le type de saisie : text-input, select, radio-grid, os-picker, auth-credential, password-input... Ensemble ouvert |
required | booléen | |
sensitive | booléen | la valeur est un identifiant de connexion : stockée chiffrée, jamais renvoyée |
defaultValue | string ou null | toujours null quand sensitive vaut true |
options | tableau de string | les valeurs exactes que vous pouvez envoyer. Vide pour une saisie libre |
choices | tableau | les mêmes valeurs avec leurs libellés. Pour un os-picker, une entrée par famille, avec ses versions |
companionKeys | tableau de string | les clés que la ligne doit porter en plus de celle-ci |
Deux cas de companionKeys :
authValue(kind: "auth-credential") demandeauthTypeà côté, valantPASSWORDouSSH_KEY.os(kind: "os-picker") demandeosVersionsi 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 prendPOST /services/{code}/reinstall.
POST /orders
Permissions : orders:write, plus celles du financement choisi.
En-tête obligatoire : Idempotency-Key.
| Champ | Type | Requis | Valeurs | D'où il vient |
|---|---|---|---|---|
lines | tableau | oui | 1 à 20 lignes | - |
lines[].product | string | oui | - | GET /catalog/products → shortname |
lines[].recurrence | string | oui quand le produit en propose | une valeur de recurrences | GET /catalog/products → recurrences |
lines[].quantity | entier | non, défaut 1 | 1 à 100 | - |
lines[].characteristics | objet string → string | selon le produit | les clés de characteristics[], valeurs prises dans options | GET /catalog/products/{shortname} |
lines[].addons[].shortname | string | oui dans une option | - | GET /catalog/products/{shortname} → addons[] |
lines[].addons[].quantity | entier | non, défaut 1 | 1 à maxQuantity | - |
promoCode | string ou null | non | - | - |
customerNote | string ou null | non | 500 caractères au plus | - |
payment | objet ou null | non, défaut aucun paiement | voir ci-dessous | - |
Les valeurs de characteristics sont toujours des chaînes, même pour un nombre.
Le bloc payment
| Champ | Type | Requis | Valeurs |
|---|---|---|---|
method | string | non, défaut none | none, balance, saved_method |
paymentMethodId | entier | oui si saved_method | GET /payment-methods → id |
method | Permissions en plus | Effet |
|---|---|---|
none, ou pas de bloc | aucune | la commande reste PENDING_PAYMENT. Payez-la ensuite |
balance | billing.invoices:pay | le solde prépayé paie. Solde trop court : 402 INSUFFICIENT_BALANCE, et aucune commande n'est créée |
saved_method | billing.invoices:pay et billing.saved_methods:charge | le 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 } } }
| Champ | Type | Notes |
|---|---|---|
order.status | string | DRAFT, PENDING_PAYMENT, AUTHORIZED, PAID, FULFILLED, CANCELLED, FAILED, REFUNDED |
order.serviceCodes | tableau de string | les services que la commande a créés. Vide tant que le paiement n'est pas réglé et le provisionnement pas fini |
order.lines[].configuration | objet string → string | la configuration retenue. Les valeurs sensitive n'y sont jamais |
promoError | string ou null | renseigné quand le code promo n'a pas pu s'appliquer. La commande tient au plein tarif |
payment.status | string | NONE 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
| Erreur | Statut | Quand |
|---|---|---|
NO_ITEMS | 400 | lines vide ou absent |
TOO_MANY_ITEMS | 400 | plus de 20 lignes |
PRODUCT_NOT_FOUND:<shortname> | 400 | produit inconnu ou privé |
PRODUCT_NOT_ORDERABLE:<shortname> | 400 | catégorie retirée de la vente |
RECURRENCE_REQUIRED:<shortname> | 400 | le produit se vend par cycles et recurrence manque |
RECURRENCE_NOT_OFFERED:<shortname> | 400 | ce cycle n'existe pas pour ce produit |
MISSING_REQUIRED:<clé> | 400 | une caractéristique obligatoire manque |
INVALID_METADATA_KEY:<clé> | 400 | une clé que le produit ne déclare pas |
INVALID_METADATA_VALUE:<clé> | 400 | une valeur hors options |
ADDON_NOT_AVAILABLE:<shortname> | 400 | option non proposée par ce produit |
ADDON_MAX_QUANTITY:<shortname> | 400 | quantité au-delà de maxQuantity |
VALIDATION_ERROR:lines.quantity | 400 | quantité hors de 1 à 100 |
VALIDATION_ERROR:customerNote | 400 | au-delà de 500 caractères |
VALIDATION_ERROR:payment.method | 400 | method hors liste |
PAYMENT_METHOD_ID_REQUIRED | 400 | saved_method sans paymentMethodId |
PAYMENT_METHOD_NOT_FOUND | 404 | moyen inconnu du compte |
PAYMENT_METHOD_NOT_ACTIVE | 400 | moyen expiré ou bloqué |
INSUFFICIENT_BALANCE | 402 | solde trop court |
PAYMENT_DENIED | 402 | la banque a refusé. La commande reste payable autrement |
API_KEY_SPENDING_CAP_EXCEEDED | 403 | le plafond de la clé ne couvre pas le total |
PAYMENT_IN_PROGRESS | 409 | un autre paiement de la même commande tourne |
PAYMENT_RECORDED_MISMATCH | 409 | anomalie de rapprochement. Contactez le support |
ORDER_CONCURRENT_UPDATE | 409 | la 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.
| Champ | Type | Requis | Valeurs | D'où il vient |
|---|---|---|---|---|
method | string | oui | balance, saved_method | - |
paymentMethodId | entier | oui si saved_method | > 0 | GET /payment-methods → id |
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.
| Erreur | Statut | Quand |
|---|---|---|
ORDER_NOT_FOUND | 404 | numéro inconnu |
ORDER_NOT_PAYABLE, ORDER_ALREADY_SETTLED, ORDER_ALREADY_PAID | 400 | il n'y a rien à payer |
AMOUNT_INVALID | 400 | le 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ètre | Type | Requis | Valeurs |
|---|---|---|---|
limit | entier | non, défaut 20 | 1 à 100 |
cursor | string | non | pagination.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
GET /orders/{orderNumber}:statuspasse àPAID, puis àFULFILLEDune fois chaque service en place.serviceCodesse remplit avec les codes créés. C'est le lien entre une commande et les services qu'elle a produits.GET /services/{code}rendprovisioned: falsetant que l'infrastructure se met en place, puistrue.
Le provisionnement tourne en arrière-plan et prend quelques minutes. Interrogez toutes les 10 à 30 secondes.