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.
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.
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é.
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"}'
{ "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.
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.
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.
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.
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.
#!/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-LimitetX-RateLimit-Remaining; un refus rend429 rate_limitedavecRetry-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 parcase, 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/accountlu avant de commander.Idempotency-Keydérivée du travail, pas de l'exécution.- Sondage de l'action avec une borne de temps, pas de boucle infinie.
trapde destruction posé avant la création.- Codes d'erreur traités par code, pas par message.
X-Request-Idjournalisé.
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.