VPS KVM canadien à Montréal dès 8,50 CAD/mois

Piloter un VPS par API : créer, suivre et détruire en script

Une machine commandée par script sert le temps d'une tâche, puis disparaît. Clé et portées, catalogue lisible sans clé, création idempotente, sondage de l'action, destruction garantie par un trap : le tout en un script de trente-cinq lignes, à 0,018 CAD l'heure.

Requête HTTP commandant la création d'une machine, et la machine livrée

Une machine commandée à la main sert longtemps. Une machine commandée par script sert le temps d'une tâche : un build, un rendu, un test de charge, une migration. Elle naît quand le travail arrive et meurt avec lui, et c'est la facturation à l'heure qui rend l'exercice sensé — une heure de Nano coûte 0,018 CAD, soit deux centièmes pour un travail qui aurait mobilisé un serveur au mois.

L'API FFxF fait exactement ce que fait la console, avec les mêmes règles : même catalogue, même quota, même crédit, mêmes refus. Ce que le navigateur interdit, l'API l'interdit aussi, et avec le même motif. Ce guide part d'un compte vide et finit sur un script complet qui commande une machine, y travaille et la détruit — y compris quand le travail échoue.

1. Une clé, et seulement les portées utiles

La clé se crée dans la console, sous Compte → Clés d'API. Elle s'affiche une seule fois : rien ne permet de la relire ensuite, seul son préfixe reste visible dans la liste. Chaque clé porte des portées, et il faut cocher le minimum — une clé de supervision n'a rien à faire avec vms.destroy. Un appel hors portée répond 403 insufficient_scope en nommant celle qui manquait.

bash
export FFXF_TOKEN='ffxf_live_…'
export API=https://api.ffxf.net/v1
export AUTH="Authorization: Bearer $FFXF_TOKEN"

curl -s "$API/account" -H "$AUTH" | jq '{solde: .data.credit.balance,
                                         brûle: .data.hourly.burn_rate,
                                         autonomie: .data.hourly.runway_hours,
                                         quota: .data.quota}'

GET /v1/account est le premier appel d'un script bien élevé : il rend le solde, la dépense horaire en cours, l'autonomie restante et l'état du quota. Décider avant de commander évite de découvrir le refus au milieu d'une chaîne d'intégration.

2. Le catalogue se lit sans clé

Régions, forfaits et images sont publics. Les identifiants sont des slugs stables — nano, debian-13, montreal — et non des numéros internes qui changeraient à la prochaine réinstallation d'hyperviseur.

bash
curl -s "$API/plans" | jq -r '.data[] |
  "\(.slug)\t\(.vcpu) vCPU  \(.memory_mb/1024) Go  \(.prices[]|select(.currency=="CAD")|.hourly) CAD/h"'
nano     1 vCPU  2 Go   0.018 CAD/h
starter  2 vCPU  4 Go   0.033 CAD/h
pro      4 vCPU  8 Go   0.062 CAD/h
scale    8 vCPU  16 Go  0.116 CAD/h

# Les images disponibles pour un forfait donné : toutes ne tiennent pas partout.
curl -s "$API/images?plan=nano" | jq -r '.data[].slug' | head

Le filtre compte : une image Windows demande plus de disque qu'un Nano n'en a, et le catalogue le dit avant la commande plutôt qu'après. Commander une image incompatible répond 422 image_incompatible_with_plan.

3. Commander, sans risquer le doublon

POST /v1/vms exige un en-tête Idempotency-Key. Ce n'est pas une formalité : un script qui perd sa connexion pendant la réponse ne sait pas si la machine existe. Avec la même clé, le second envoi rend la première réponse — et Idempotency-Replayed: true — au lieu de créer une seconde machine. Une valeur par commande, pas par exécution : le numéro du build fait une très bonne clé.

bash
BUILD=4821
curl -s -X POST "$API/vms" -H "$AUTH" \
     -H "Idempotency-Key: build-$BUILD" \
     -H 'Content-Type: application/json' \
     -d '{"plan":"nano","region":"montreal","image":"debian-13",
          "hostname":"runner-'"$BUILD"'","billing":"hourly",
          "ssh_keys":["SHA256:0mR1vP…"],"password_delivery":"none"}'
bash
{ "data": {
    "vm":     { "id": 4312, "hostname": "runner-4821", "status": "provisioning",
                "billing": { "mode": "hourly", "hourly": { "rate": "0.018",
                             "hours_billed": 1, "amount_billed": "0.018" } } },
    "action": { "id": 90112, "type": "create", "status": "running", "vm_id": 4312 },
    "invoice": null } }

La réponse est un 202 : la machine est commandée, pas encore livrée. L'heure est prélevée à son début, donc la première est due dès la commande. Une location à l'heure n'est acceptée que si le solde couvre les 24 premières heures — sinon 402 insufficient_credit, avec le montant manquant au cent près.

Pour vérifier sans rien engager, le champ dry_run fait passer tous les contrôles et rend le prix, le crédit nécessaire et l'autonomie qui resterait après. Utile en préproduction, et utile pour expliquer un refus à l'appelant.

bash
curl -s -X POST "$API/vms" -H "$AUTH" -H "Idempotency-Key: essai-$BUILD" \
     -H 'Content-Type: application/json' \
     -d '{"plan":"nano","region":"montreal","image":"debian-13",
          "hostname":"essai","billing":"hourly","dry_run":true}' \
  | jq '.data | {ok: .would_succeed, requis: .required_credit, apres: .runway_hours_after}'

4. Attendre la livraison

Provisionner, redémarrer, réinstaller : tout ce qui touche l'hyperviseur est asynchrone. Ces appels rendent un objet action que l'on sonde jusqu'à ce que son statut quitte queued puis running. Aucune action ne reste ouverte indéfiniment : passé six heures sans conclusion, elle bascule en error, et la boucle sort.

bash
attendre_action() {
  local id=$1 statut
  for _ in $(seq 1 120); do
    statut=$(curl -s "$API/actions/$id" -H "$AUTH" | jq -r .data.status)
    case "$statut" in
      completed) return 0 ;;
      error)     curl -s "$API/actions/$id" -H "$AUTH" | jq -r .data.error.message >&2
                 return 1 ;;
    esac
    sleep 5
  done
  echo "action $id toujours ouverte après 10 minutes" >&2; return 1
}

Cinq secondes entre deux sondages suffisent : une machine est livrée en une quinzaine de secondes. Le seau de lecture autorise 120 appels par minute, mais rien n'oblige à les consommer.

5. Piloter la machine

Les changements d'état passent tous par le même point d'entrée, avec le type en corps de requête. shutdown demande un arrêt propre à l'invité, stop coupe l'alimentation — le second ne se justifie que lorsque le premier n'aboutit pas.

bash
curl -s -X POST "$API/vms/4312/actions" -H "$AUTH" \
     -H 'Content-Type: application/json' -d '{"type":"reboot"}'

# Réinstaller sur une autre image : le disque est détruit et refait.
curl -s -X POST "$API/vms/4312/actions" -H "$AUTH" \
     -H 'Content-Type: application/json' \
     -d '{"type":"reinstall","image":"ubuntu-24-04",
          "ssh_keys":["SHA256:0mR1vP…"],"password_delivery":"none"}'

# Les métriques : une série de points, la part de vCPU va de 0 à 1.
curl -s "$API/vms/4312/metrics?timeframe=hour" -H "$AUTH" \
  | jq -r '.data.series[-1] | "\(.time)  cpu \(.cpu)  mem \(.memory_bytes)"'

curl -s "$API/vms/4312/network" -H "$AUTH" | jq -r '.data.ipv4[].address'

L'historique d'une machine se lit sur GET /v1/vms/{id}/actions, et il est complet : un clic dans la console y figure aussi, avec source: console. Pratique quand un collègue a redémarré la machine pendant que le script attendait.

6. Détruire, et compter

La destruction demande le nom d'hôte exact en paramètre confirm. C'est volontaire : un identifiant numérique se trompe de chiffre sans qu'on le voie, un nom d'hôte non.

bash
curl -s -X DELETE "$API/vms/4312?confirm=runner-4821" -H "$AUTH"
curl -s "$API/usage?vm_id=4312&group_by=vm" -H "$AUTH" \
  | jq -r '.data[] | "\(.hostname)  \(.hours) h  \(.amount) \(.currency)"'
runner-4821  1 h  0.018 CAD

Une heure entamée est due. Une machine créée à 14 h 05 et détruite à 14 h 40 coûte une heure, pas trente-cinq minutes — le compteur suit la minute de création, pas l'heure ronde. GET /v1/usage rend le détail ligne à ligne, et c'est la même source que la facture.

7. Le script complet

Un trap sur EXIT est ce qui sépare un runner éphémère d'une facture surprise : la machine est détruite même si le travail échoue, même si le script est interrompu.

bash
#!/usr/bin/env bash
set -euo pipefail

API=https://api.ffxf.net/v1
AUTH="Authorization: Bearer $FFXF_TOKEN"
NOM="runner-$(date +%s)"
VM=""

detruire() {
  [ -n "$VM" ] || return 0
  echo "destruction de $NOM"
  curl -s -X DELETE "$API/vms/$VM?confirm=$NOM" -H "$AUTH" > /dev/null
}
trap detruire EXIT

VM=$(curl -s -X POST "$API/vms" -H "$AUTH" -H "Idempotency-Key: $NOM" \
          -H 'Content-Type: application/json' \
          -d "{\"plan\":\"nano\",\"region\":\"montreal\",\"image\":\"debian-13\",
               \"hostname\":\"$NOM\",\"billing\":\"hourly\",
               \"ssh_keys\":[\"$FFXF_SSH_FINGERPRINT\"],\"password_delivery\":\"none\"}" \
     | jq -r .data.vm.id)

until [ "$(curl -s "$API/vms/$VM" -H "$AUTH" | jq -r .data.status)" = "running" ]; do
  sleep 5
done

IP=$(curl -s "$API/vms/$VM" -H "$AUTH" | jq -r .data.ipv4)
until ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=5 \
          root@"$IP" true 2>/dev/null; do sleep 3; done

ssh root@"$IP" 'apt-get -qq update && apt-get -qq install -y build-essential'
ssh root@"$IP" 'bash -s' < ./tache.sh
scp root@"$IP":/tmp/resultat.tar.gz ./

Trente-cinq lignes, une machine dédiée pour la durée d'un travail, et 0,018 CAD au compteur. La même trame sert à une flotte : boucler sur les commandes, garder les identifiants, détruire en fin de lot.

8. Les garde-fous à connaître

Ils existent pour que l'emballement d'un script coûte un refus et non une facture. Tous sont lisibles dans la réponse plutôt que devinés.

  • Débit : par clé, 120 lectures par minute, 20 actions par minute, 10 créations ou réinstallations par heure. Chaque réponse porte X-RateLimit-Limit et X-RateLimit-Remaining ; un refus rend 429 rate_limited avec Retry-After.
  • Quota : cinq machines par compte par défaut, les supprimées ne comptent pas. Au-delà, 409 quota_reached. Le plafond se lève par ticket.
  • Crédit : 24 heures d'avance exigées à la commande, et un plafond de budget mensuel à 120 % de la dépense prévue.
  • Codes stables : un refus porte un code lisible par machine — insufficient_credit, hostname_taken, out_of_stock — à traiter par case, pas par lecture du message.
  • Traçabilité : chaque réponse porte un X-Request-Id. Le citer dans un ticket évite une demi-journée d'allers-retours.

Checklist

  • Clé créée avec les seules portées nécessaires, stockée hors du dépôt.
  • GET /v1/account lu avant de commander.
  • Idempotency-Key dérivée du travail, pas de l'exécution.
  • Sondage de l'action avec une borne de temps, pas de boucle infinie.
  • trap de destruction posé avant la création.
  • Codes d'erreur traités par code, pas par message.
  • X-Request-Id journalisé.

La référence des vingt-huit points d'entrée, le catalogue complet des images et le contrat OpenAPI sont dans la documentation de l'API. Le contrat se donne tel quel à un générateur de client : il n'y a pas de SDK à attendre.

Support & discussions

Questions techniques, retours d'incidents ou discussions d'infrastructure, l'équipe est présente sur Discord, Telegram, X et IRC.