API REST

API REST

L'API JSON par équipe derrière le CLI : applications, variables d'environnement, déploiements, logs et serveurs.

Une petite API JSON par équipe pour piloter Railyard depuis un CLI, une tâche CI ou un script : lister et créer des applications, définir des variables d’environnement, déclencher des déploiements, lire les logs de déploiement, vérifier la flotte.

  • URL de base : votre plan de contrôle, sous /api. Compose local : http://localhost:3000/api
  • Format : JSON seulement. Envoyez Content-Type: application/json pour les écritures. Aucun CSRF, aucune session, aucun cookie.
  • Tout est limité à une seule équipe : l’équipe à laquelle appartient le jeton.

Authentification

Chaque requête a besoin d’un jeton porteur :

Authorization: Bearer rly_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Obtenir un jeton : tableau de bord → Team → API tokens → nommez-le, choisissez une portée, Create. La chaîne rly_… ne s’affiche qu’une seule fois : copiez-la à ce moment-là. Réservé aux administrateurs.

Portées

PortéePermet
readtous les GET
writetout ce que permet read, plus POST / PATCH / DELETE

Un jeton en lecture seule sur un point de terminaison qui modifie des données → 403 {"error":"this API token is read-only"}. Jeton manquant, invalide, révoqué ou expiré → 401 {"error":"missing or invalid API token"}.

Révoquez-le depuis la même page Team; un jeton révoqué ou dont la date expires_at est passée arrête de fonctionner immédiatement.


Conventions

  • Erreurs : les réponses autres que 2xx sont { "error": "<message>" }. Codes de statut : 400 requête invalide · 401 non authentifié · 403 mauvaise portée · 404 introuvable (ou pas dans votre équipe) · 409 conflit (par ex. un déploiement est déjà en cours) · 422 validation échouée · 429 limite de débit.
  • Horodatages : ISO-8601 UTC (2026-09-02T14:03:11Z).
  • ID : des entiers, stables par équipe.
  • Pagination : les points de terminaison de liste qui acceptent limit acceptent 1 à 100 (30 par défaut). Pas encore de curseur; du plus récent au plus ancien quand c’est classé par date.
  • Limite de débit : la création de déploiements est limitée par équipe et par heure (GET /api/me → limits.deploys_per_hour); au-delà → 429.

Points de terminaison

MéthodeCheminPortéeDescription
GET/api/mereadInfos sur le jeton et l’équipe, limites, décomptes
GET/api/applicationsreadLister les applications
POST/api/applicationswriteCréer une application
GET/api/applications/:idreadDétail de l’application
PATCH/api/applications/:idwriteMettre à jour une application
DELETE/api/applications/:idwriteSupprimer une application (et ses déploiements et logs)
POST/api/applications/:id/archivewriteArrêter le conteneur, conserver l’enregistrement
POST/api/applications/:id/unarchivewriteDésarchiver
GET/api/applications/:id/envreadLire les variables d’environnement
PATCH/api/applications/:id/envwriteFusionner les variables d’environnement (une valeur null supprime)
DELETE/api/applications/:id/env?key=KEYwriteSupprimer une variable d’environnement
GET/api/applications/:id/edgereadParamètres de périphérie (même structure que la section edge: du manifeste, voir Règles de périphérie)
PATCH/api/applications/:id/edgewriteFusionner les paramètres de périphérie : {"edge":{"access_logs":false,"headers":[…],"cors":{…}}}
GET/api/applications/:id/certificatesreadCertificats téléversés (noms, hôtes couverts, validité, empreinte; jamais la clé)
POST/api/applications/:id/certificateswriteTéléverser {"certificate":{"certificate_pem":"…","private_key_pem":"…"}}
DELETE/api/applications/:id/certificates/:cert_idwriteEn supprimer un (les certificats automatiques reprennent après le prochain déploiement)
GET/api/applications/:id/domainsreadLister les domaines personnalisés
POST/api/applications/:id/domainswriteAjouter un domaine personnalisé
DELETE/api/applications/:id/domains/:domain_idwriteSupprimer un domaine personnalisé
GET/api/applications/:id/deploysreadDéploiements d’une application
POST/api/applications/:id/deployswriteDéclencher un déploiement ({"image": "registry/repo:tag"} déploie cette image préconstruite une fois)
GET/api/applications/:id/registrywriteLe registre de build de l’équipe pour deploy --local : repository, username, password, platform
POST/api/applications/:id/tunnelwriteOuvrir un tunnel ({"target": "web|db|redis|<process>", "local_port": 5432}) : retourne un ticket valide 12 heures pour le WebSocket TunnelChannel, le port distant, et une url localhost pour les bases de données; consigné comme app.tunnel_opened
GET/api/applications/:id/logsreadLogs du conteneur en exécution (?tail=, ?process=)
GET/api/applications/:id/processesreadÉtat par conteneur (pour ps)
POST/api/applications/:id/restartwriteRedémarrer tous les conteneurs, ou un seul avec ?process=
GET/api/applications/:id/process_typesreadTypes de processus avec les paramètres d’environnement, d’arrêt et de taille
PATCH/api/applications/:id/process_types/:pt_idwriteDéfinir env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit
GET/api/applications/:id/commandsreadExécutions récentes de commandes ponctuelles
POST/api/applications/:id/commandswriteExécuter une commande ponctuelle ({"command": "..."})
GET/api/applications/:id/commands/:cmd_idreadUne exécution : statut, sortie, code de sortie
POST/api/applications/:id/reindexwriteReconstruire les index de recherche : exécute le search.reindex_command de l’application dans un conteneur ponctuel (202; 422 sans commande ou sans déploiement réussi, 409 si une réindexation est déjà en cours). Définissez reindex_command / auto_reindex avec PATCH /api/applications/:id; GET les affiche sous search avec la library détectée
POST/api/applications/:id/clonewriteCloner l’application dans une copie staging
POST/api/applications/:id/promotewritePromouvoir la version d’une application staging vers la production
GET/api/applications/:id/manifestreadL’application sous forme de railyard.yml (?secrets=true inclut les valeurs des variables)
POST/api/applications/:id/apply_manifestwriteAppliquer un manifeste ({"manifest": "<yaml text>"})
POST/api/applications/validate_manifestreadVérifier un manifeste sans l’appliquer — sans :id, le jeton de n’importe quelle application fonctionne ({"manifest": "<yaml text>"} → {"problems": [...]}, vide s’il est valide)
POST/api/applications/:id/db_queryread / writeExécuter du SQL ({"sql": "...", "write": false}); write: true exige un jeton à portée write
GET/POST/api/applications/:id/database_backupsread / writeLister les sauvegardes ou en faire une maintenant
POST/api/database_backups/:id/restorewriteRestaurer une sauvegarde
GET/api/database_backups/:id/downloadreadLien de téléchargement signé
GET/POST/api/applications/:id/volume_backupsread / writeLister les sauvegardes ou sauvegarder un volume ({"volume": "name"})
POST/api/volume_backups/:id/restorewriteRestaurer une sauvegarde de volume
GET/api/applications/:id/trafficreadRequêtes, p95, taux d’erreur et points de terminaison les plus lents (?range=1h|24h|7d|30d)
PATCH/api/applications/:id/autoscalewriteAjuster le nombre de répliques web selon p95/CPU ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} désactive la fonction
POST/api/applications/:id/data_movewriteDéplacer les données de l’application vers un serveur de base de données ({"database_server_id": 12}); 202 avec le détail de ce qui est copié
POST/api/applications/:id/external_datastoreswritePointer database, redis ou elasticsearch de l’application vers une URL externe ({"kind","url","copy"}); l’URL est d’abord vérifiée (502 si injoignable), 202 une fois mis en file d’attente
DELETE/api/applications/:id/external_datastores/:kindwriteRamener ces données sous Railyard (?copy=true les copie de retour)
GET/api/applications/:id/addons/:kind/transfersreadExports et imports récents d’un module complémentaire (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), avec liens de téléchargement
POST/api/applications/:id/addons/:kind/transferswriteExporter les données du module complémentaire; avec un file multipart et confirm=<app name>, y charger un fichier
GET/api/applications/:id/storage/objectsreadParcourir le bucket du module complémentaire de stockage (?prefix=, ?token=) : dossiers, fichiers avec taille, indicateur public et lien signé
POST/api/applications/:id/storage/objectswriteTéléverser un fichier (file multipart, prefix ou key)
DELETE/api/applications/:id/storage/object?key=writeSupprimer un fichier, ou un dossier si la clé se termine par /
PATCH/api/applications/:id/storage/visibilitywriteRendre un dossier public ou privé ({"prefix":"avatars/","public":true})
GET/api/applications/:id/redisreadOù réside le Redis de l’application (base partagée N ou la sienne), sa limite et sa politique, mémoire utilisée
PATCH/api/applications/:id/rediswrite{"dedicated":true,"maxmemory_mb":512,"policy":"allkeys-lru"} donne à l’application son propre Redis (les clés sont copiées) ou change sa limite; {"dedicated":false} la ramène en arrière
GET/api/deploysreadDéploiements de toute l’équipe (?application_id=, ?limit=)
GET/api/deploys/:idreadDétail du déploiement
GET/api/deploys/:id/logsreadLignes de logs du déploiement (du plus ancien au plus récent, ≤5000)
GET/api/applications/:id/log_searchreadLignes de logs enregistrées : plage de temps, recherche, pages par curseur (voir Logs, métriques et alertes)
GET/api/applications/:id/log_downloadreadLignes de logs enregistrées pour une plage, en texte
GET/PATCH/DELETE/api/applications/:id/otel_exportread / writeParamètres d’export OpenTelemetry (voir Logs, métriques et alertes)
GET/POST/api/applications/:id/metric_alertsread / writeAlertes de seuil de métrique et leur historique
PATCH/DELETE/api/applications/:id/metric_alerts/:alert_idwriteModifier ou supprimer une alerte
GET/api/applications/:id/job_queuereadStatistiques de la file de tâches, tâches échouées, historique (voir Logs, métriques et alertes)
POST/api/applications/:id/job_queue/failed/:job_id/retrywriteRéessayer une tâche échouée
DELETE/api/applications/:id/job_queue/failed/:job_idwriteAbandonner une tâche échouée
GET/api/serversreadLa flotte
GET/api/servers/:idreadDétail du serveur, états des processus en direct
POST/api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_dockerwriteActions sur le cycle de vie du serveur
PATCH/api/servers/:id/resizewriteRedimensionner chez le fournisseur ({"size": "..."})
GET/api/notification_rulesreadRègles de notification à l’échelle de l’équipe
GET/api/webhook_endpointsreadPoints de terminaison webhook à l’échelle de l’équipe
GET/api/configreadEntrées de configuration à l’échelle de l’équipe

GET /api/me

curl -s https://cp.example.com/api/me -H "Authorization: Bearer $RLY"
{
  "team":  { "id": 1, "name": "Acme", "slug": "acme", "suspended": false },
  "token": { "name": "ci", "scope": "write", "last_used_at": "2026-09-02T14:00:00Z" },
  "limits": { "servers": 10, "apps": 50, "deploys_per_hour": 30 },
  "counts": { "applications": 11, "servers": 3 }
}

Applications

Objet app (sous forme de liste) :

{
  "id": 11,
  "name": "chatwoot-copy",
  "environment": "production",
  "parent_id": null,
  "git_url": "image://chatwoot/chatwoot:v4.17.1",
  "git_ref": "main",
  "hostname": "chatwoot-copy.apps.example.com",
  "url": "https://chatwoot-copy.apps.example.com",
  "server": "railyard-8",
  "container_port": 3000,
  "addons": ["postgres", "redis"],
  "archived": false,
  "auto_deploy": false,
  "latest_deploy": { "id": 31, "status": "succeeded", "sha": null, "created_at": "2026-09-02T13:00:00Z" },
  "created_at": "2026-09-01T09:00:00Z"
}

GET /api/applications/:id ajoute : release_command, health_path, deploy_approval_required, deploy_window {days, start, end, time_zone, open_now, opens_at}, strict_pushes, db_extensions[], processes[] {name, command, role}, env_keys[] (les noms seulement : les valeurs ne sont jamais retournées dans le détail de l’application; utilisez le point de terminaison env), domains[] {hostname, redirect, cert_state}.

Créer : POST /api/applications

curl -s -X POST https://cp.example.com/api/applications \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"application":{
        "name":"blog",
        "git_url":"https://github.com/acme/blog",
        "git_ref":"main",
        "container_port":3000,
        "server":"railyard-8",
        "addons":["postgres"],
        "env_vars":{"RAILS_ENV":"production"}
      }}'

Le corps est {"application": { … }}. Clés acceptées : name, git_url, git_ref, container_port, hostname, health_path, release_command, build_method (auto | dockerfile | nixpacks), dockerfile_path, docker_build_target, cpu_limit, memory_limit, process_types_text (un name: command par ligne), scheduled_tasks_text (name | cron | command par ligne), db_extensions, deploy_approval_required, strict_pushes (un git push dont le build échoue est rejeté; true par défaut), addons (tableau), env_vars (objet). Également à la mise à jour : server_id.

  • server (ou server_id) peut être un ID de serveur ou un nom d’hôte. Si vous l’omettez, l’application est placée sur le premier serveur non mis en quarantaine de l’équipe.
  • Si hostname est vide, il est déduit : <name>.<server app_domain>, sinon APP_DOMAIN_SUFFIX, sinon <name>.<server-ip>.sslip.io.
  • La création ne déclenche pas de déploiement. Enchaînez avec POST …/deploys.
  • Au-delà de la limite d’applications de l’équipe → 422.

201 avec l’objet détaillé de l’application. PATCH → 200. DELETE → 204.

Archiver / désarchiver : POST /api/applications/:id/archive arrête le conteneur en cours d’exécution et met archived: true; l’enregistrement, la configuration et l’historique restent. unarchive retire l’indicateur (redéployez pour la faire revenir).


Variables d’environnement — /api/applications/:id/env

# read
curl -s https://cp.example.com/api/applications/11/env -H "Authorization: Bearer $RLY"
# {"env":{"RAILS_ENV":"production","LOG_LEVEL":"info"}}

# merge (set two, delete one)
curl -s -X PATCH https://cp.example.com/api/applications/11/env \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"env":{"LOG_LEVEL":"debug","FEATURE_X":"1","OLD_KEY":null}}'

# delete one
curl -s -X DELETE "https://cp.example.com/api/applications/11/env?key=FEATURE_X" \
  -H "Authorization: Bearer $RLY"

PATCH fusionne avec l’ensemble existant; une valeur null retire cette clé. Les trois retournent l’objet complet {"env": { … }}. Les changements prennent effet au prochain déploiement.


Domaines personnalisés — /api/applications/:id/domains

curl -s -X POST https://cp.example.com/api/applications/11/domains \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"domain":{"hostname":"support.acme.com","redirect":false}}'
{ "id": 4, "hostname": "support.acme.com", "redirect": false,
  "cert_state": "pending", "dns_target": "203.0.113.9", "cert_issuer": null }

redirect: true fait en sorte que le nom d’hôte redirige en 301 vers le nom d’hôte principal de l’application au lieu de le servir. Pointez le dns_target affiché vers votre DNS et Caddy émet le certificat. DELETE …/domains/:domain_id → 204.


Déploiements

Objet deploy :

{
  "id": 31, "application_id": 11, "application": "chatwoot-copy",
  "status": "succeeded", "ref": "main", "trigger": "manual", "message": null, "sha": null, "rollback": false,
  "created_at": "2026-09-02T13:00:00Z",
  "started_at": "2026-09-02T13:00:04Z",
  "finished_at": "2026-09-02T13:02:31Z"
}

status ∈ queued · pending_approval · running · succeeded · failed. GET /api/deploys/:id ajoute error, triggered_by (courriel), duration_seconds.

Un déploiement canary a aussi un objet canary (null autrement) :

"canary": {
  "percent": 10, "state": "running", "started_at": "2026-09-29T12:00:00Z",
  "promote_at": "2026-09-29T12:15:00Z", "requests": 412, "error_rate": 0.49, "p95_ms": 88.4,
  "rest_of_app": { "requests": 3710, "error_rate": 0.4, "p95_ms": 91.0 }
}

state ∈ running · promoted · rolled_back · replaced.

Canary : POST /api/deploys/:id/canary/promote démarre le déploiement de promotion (202, promotion_deploy_id); DELETE /api/deploys/:id/canary effectue un retour arrière. Les deux exigent un jeton write et répondent 409 dès que le canary n’est plus actif.

Stratégie de déploiement : PATCH /api/applications/:id/deploy_strategy avec un ou plusieurs de deploy_strategy (standard · canary), canary_percent (1 à 99), canary_steps_text (pourcentages croissants séparés par des virgules, par ex. "5, 25, 50"; vide = une seule étape), canary_promote_after_minutes (1 à 1440, vide = manuellement), canary_max_error_rate (0 à 100). GET /api/applications/:id affiche les mêmes champs. Avec des étapes définies, promouvoir une étape démarre la suivante comme son propre canary au lieu de passer directement à 100 %.

Redéployer quand l’image change (applications image seulement, sinon 422) : PATCH /api/applications/:id/image_watch avec enabled (true/false); POST /api/applications/:id/image_watch/check vérifie le registre immédiatement (202). Les deux le retournent, et GET /api/applications/:id l’affiche sous image_watch :

{ "enabled": true, "image": "ghcr.io/acme/api:prod",
  "webhook_url": "https://cp.example.com/webhooks/image/2cV…",
  "digest": "sha256:fb84…", "checked_at": "2026-09-29T12:05:00Z", "error": null }

Les registres appellent POST /webhooks/image/<token> (aucun jeton d’API; n’importe quel corps) pour déclencher une vérification.

Déclencher : POST /api/applications/:id/deploys

curl -s -X POST https://cp.example.com/api/applications/11/deploys \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"ref":"v2.1.0"}'
  • ref (optionnel) : branche, étiquette ou SHA à déployer une seule fois; par défaut, la git_ref de l’application, qu’un ref ne change jamais. Le trigger du déploiement est alors ref et son champ ref indique ce qui a été utilisé. Les noms contenant des caractères que git n’accepte pas dans une référence reçoivent 422. Ignoré pour une application image://.
  • message (optionnel) : une note de déploiement (jusqu’à 500 caractères), retournée dans message et affichée dans le tableau de bord, l’activité, les notifications et les webhooks deploy.*.
  • at (optionnel) : heure ISO 8601 à laquelle déployer au lieu de tout de suite. Répond 201 avec status: "scheduled" et scheduled_for; l’heure doit être dans le futur et à l’intérieur de la fenêtre de déploiement (422 autrement). Annulez-le avec POST /api/deploys/:id/cancel (annule aussi un déploiement en file ou en cours).
  • ignore_window (optionnel) : déployer même si l’application est hors de sa fenêtre de déploiement. Sans ce paramètre, un tel déploiement répond 409 avec outside_window: true et window_opens_at.
  • rollback_to (optionnel) : ID d’un déploiement réussi antérieur. Redéploie son commit et rétablit les variables avec lesquelles il s’est exécuté; la réponse ajoute restored_config. Envoyez "restore_config": false pour revenir en arrière sur le code seulement. Les versions déployées avant que Railyard ne conserve une copie de leurs variables ne reviennent en arrière que sur le code.
  • 202 Accepted + l’objet déploiement quand il est en file d’attente ou en cours d’exécution.
  • 200 OK + l’objet avec status: "pending_approval" si l’application exige une approbation de déploiement (approuvez-le dans le tableau de bord).
  • 409 si un déploiement est déjà actif ou en attente d’approbation pour cette application.
  • 429 si la limite de déploiements par heure de l’équipe est atteinte.
  • 422 si l’application est archivée.
  • 422 avec missing_env (tableau de noms) si l’application n’a jamais été déployée avec succès et que des identifiants documentés par son dépôt ne sont pas définis ou sont encore des valeurs d’exemple. Définissez-les via le point de terminaison env, ou envoyez "force": true pour déployer quand même : ces noms ne seront plus redemandés.

Fenêtre de déploiement : PATCH /api/applications/:id/deploy_window

curl -s -X PATCH https://cp.example.com/api/applications/11/deploy_window \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"days":["mon","tue","wed","thu","fri"],"start":"09:00","end":"17:00","time_zone":"America/Toronto"}'

Retourne {days, start, end, time_zone, open_now, opens_at} (aussi sous deploy_window dans GET /api/applications/:id). "days": [] retire la fenêtre. Les push, les webhooks GitHub/GitLab et les push regroupés hors de la fenêtre deviennent un seul déploiement scheduled qui démarre à l’ouverture de la fenêtre.

Logs : GET /api/deploys/:id/logs

{
  "deploy_id": 31,
  "status": "succeeded",
  "lines": [
    { "stream": "system", "line": "Deploying web (web)", "at": "2026-09-02T13:00:05Z" },
    { "stream": "stdout", "line": "Pulled chatwoot/chatwoot:v4.17.1", "at": "2026-09-02T13:00:40Z" },
    { "stream": "system", "line": "Deployed at https://chatwoot-copy.apps.example.com", "at": "2026-09-02T13:02:30Z" }
  ]
}

stream ∈ stdout · stderr · system. Limité à 5000 lignes, les plus anciennes en premier. Interrogez tant que status est queued/running.

Liste : GET /api/deploys?application_id=11&limit=50 ou GET /api/applications/11/deploys.


Exécution — logs, processus, commandes, redémarrage

La moitié programmable de la console de la page d’application. Tout ceci passe par l’agent sur la machine via un canal mTLS.

Logs d’exécution : GET /api/applications/:id/logs?tail=200&process=web

Ce que l’application en cours d’exécution imprime en ce moment, pas la sortie du build (c’est /api/deploys/:id/logs). tail va de 1 à 5000 (200 par défaut); process filtre sur un seul processus. 409 avec {"error": "No running deployment"} quand rien n’est encore en ligne, ou que l’agent est injoignable.

{
  "application_id": 11,
  "lines": [
    { "stream": "stdout", "process": "web", "line": "GET / 200 12ms", "at": "2026-09-06T12:00:01Z" }
  ]
}

Il n’y a pas encore de suivi par poussée serveur : railyard logs --follow relance une requête à intervalles réguliers.

Processus : GET /api/applications/:id/processes

Dernier état signalé de chaque conteneur, actualisé à chaque pulsation de l’agent. Liste vide = rien en cours d’exécution.

{ "processes": [
  { "name": "web", "container": "railyard-app-11-web", "state": "running",
    "status": "running", "health": "healthy", "crash_looping": false,
    "restarts": 0, "cpu_percent": 1.2, "mem_bytes": 92210000,
    "mem_percent": 44.1, "last_seen_at": "2026-09-06T12:00:00Z" } ] }

status ∈ running · unhealthy · crash-looping · exited · dead · …

Redémarrage : POST /api/applications/:id/restart[?process=web]

Redémarre tous les conteneurs, ou seulement celui d’un processus. 409 si rien n’est en cours d’exécution.

{ "restarted": 2, "processes": ["railyard-app-11-web", "railyard-app-11-worker-1"] }

Types de processus : GET /api/applications/:id/process_types, PATCH /api/applications/:id/process_types/:pt_id

{ "id": 7, "name": "worker", "role": "worker", "command": "bundle exec sidekiq",
  "replicas": 2, "env_vars": {}, "kill_timeout_seconds": 30, "stop_signal": "",
  "size": "small", "cpu_limit": "0.5", "memory_limit": "512m", "cpus": "0.5", "memory": "512m" }

size est small, medium, large (mappé depuis PROCESS_SIZE_*), custom (envoyez alors cpu_limit et memory_limit) ou vide pour la limite globale de l’application. cpus / memory sont les limites effectives après ce repli.

kill_timeout_seconds (1 à 300, vide pour la valeur par défaut) est le temps dont dispose le processus après stop_signal (SIGTERM, SIGINT, SIGQUIT, vide pour celui de l’image) avant d’être tué. Les changements s’appliquent au prochain déploiement. Une valeur invalide → 422.

Commandes ponctuelles : POST /api/applications/:id/commands

curl -s -X POST https://cp.example.com/api/applications/11/commands \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"command":"bin/rails db:migrate"}'

S’exécute dans le conteneur web via le RPC RunTask de l’agent. 202 Accepted avec la ligne; interrogez GET /api/applications/:id/commands/:cmd_id pour le status (queued → running → succeeded/failed), le output en continu, et le exit_code. GET /api/applications/:id/commands liste les exécutions récentes.

La console interactive (un TTY, RPC Exec) n’est pas encore dans l’API.


Serveurs (lecture seulement)

curl -s https://cp.example.com/api/servers -H "Authorization: Bearer $RLY"

Objet server :

{
  "id": 5, "hostname": "railyard-8", "name": "railyard-8",
  "status": "online",
  "agent_version": "20260902-a9821d1",
  "cpu_percent": 1.5, "memory_percent": 64.5, "disk_percent": 40.3,
  "last_heartbeat_at": "2026-09-02T14:03:00Z",
  "cordoned": false
}

status ∈ online (pulsation < 30 s) · stale (< 5 min) · offline. GET /api/servers/:id ajoute apps[] (noms) et processes[] {container, state, restarts, crash_looping}.

Mettre en quarantaine, redémarrer, effectuer un cycle d’alimentation, purger et redimensionner un serveur existant se fait en POST/PATCH sur /api/servers/:id (voir le tableau plus haut). Attacher un nouveau serveur n’est possible que depuis le tableau de bord pour l’instant.

Fenêtre de maintenance des données : GET/PATCH /api/maintenance_window (équipe) et GET/PATCH /api/servers/:id/maintenance_window (un serveur; jour vide = utiliser celle de l’équipe). Champs : maintenance_day 0 à 6 (dimanche = 0), maintenance_hour 0 à 23 UTC, maintenance_hours 1 à 12. Voir Serveurs de base de données.

Mises à niveau majeures de Postgres : GET/POST /api/servers/:id/postgres_upgrades (to_image; la création en lance la vérification de compatibilité), GET …/:id (avec le log), PATCH …/:id (when=window pour attendre la fenêtre de maintenance, sinon elle démarre immédiatement), POST …/:id/rollback, DELETE …/:id (retire l’ancienne version conservée, ou abandonne un plan non démarré).

Regroupeur de connexions : GET/POST/DELETE /api/servers/:id/connection_pooler (PgBouncer, mode transaction, port 6432). Par application : PATCH /api/applications/:id/database avec app_database[pooled]=true pointe DATABASE_URL vers le regroupeur à partir du prochain déploiement et garde DATABASE_DIRECT_URL pour les migrations.

Forks de base de données : GET/POST /api/applications/:id/database_forks (target_server_id pour une nouvelle base de données ou target_application_id pour un environnement staging/aperçu, source=live|backup, mask_sql optionnel), GET …/:id (avec l’URL du fork une fois prêt), DELETE …/:id (le supprime).

Répliques de secours : GET/POST /api/servers/:id/database_replicas (replica_server_id), POST …/:id/promote, DELETE …/:id.


Cloud

Railyard Cloud transforme le compte Railyard d’une équipe en son propre petit cloud : serveurs, images, adresses statiques, une zone DNS et des buckets S3, facturés au coût cloud plus une majoration publiée. C’est conçu pour des outils comme AliaJS qui attendent l’API d’un fournisseur cloud, donc les structures suivent celles de Hetzner : chaque réponse enveloppe son objet ({"server": {...}}, {"servers": [...]}). Une carte de crédit doit être enregistrée (ajoutez-la sur la page Cloud), et les points de terminaison répondent 503 jusqu’à ce que l’opérateur active Railyard Cloud.

MéthodeCheminPortéeDescription
GET/api/cloud/sizesreadTailles de serveur avec coût horaire, majoration et prix
GET/api/cloud/ssh_keysreadLister les clés SSH
POST/api/cloud/ssh_keyswriteAjouter une clé SSH (201; 200 si la même clé existe déjà)
DELETE/api/cloud/ssh_keys/:idwriteSupprimer une clé SSH
GET/api/cloud/serversreadLister les serveurs actifs
POST/api/cloud/serverswriteLancer un serveur (201, pending)
GET/api/cloud/servers/:idreadDétail du serveur, actualisé à la lecture
DELETE/api/cloud/servers/:idwriteTerminer un serveur (200, terminated)
POST/api/cloud/servers/:id/powerwriteDémarrer un serveur arrêté
DELETE/api/cloud/servers/:id/powerwriteArrêter un serveur en cours d’exécution
POST/api/cloud/servers/:id/imageswriteCapturer un serveur dans une image (201, creating)
GET/api/cloud/imagesreadLister les images (?name= filtre)
GET/api/cloud/images/:idreadDétail de l’image, actualisé à la lecture
DELETE/api/cloud/images/:idwriteSupprimer une image
GET/api/cloud/addressesreadLister les adresses statiques
POST/api/cloud/addresseswriteAllouer une adresse statique
GET/api/cloud/addresses/:idreadDétail de l’adresse
DELETE/api/cloud/addresses/:idwriteLibérer une adresse statique
POST/api/cloud/addresses/:id/assignmentwriteL’attacher à un serveur ({"server_id": 9})
DELETE/api/cloud/addresses/:id/assignmentwriteLa détacher
GET/api/cloud/zonereadLa zone DNS de l’équipe
GET/api/cloud/zone/recordsreadLister les enregistrements
PUT/api/cloud/zone/records/:type/:namewriteCréer ou remplacer un enregistrement
DELETE/api/cloud/zone/records/:type/:namewriteSupprimer un enregistrement
GET/api/cloud/bucketsreadLister les buckets
POST/api/cloud/bucketswriteCréer un bucket; la réponse contient la clé de stockage de l’équipe
DELETE/api/cloud/buckets/:idwriteSupprimer un bucket (doit être vide)
GET/api/cloud/usagereadLes lignes d’utilisation du mois avec coût, majoration et prix

Les corps de requête sont en JSON et peuvent être plats ({"name": "web"}) ou enveloppés ({"server": {"name": "web"}}). Les jetons à portée application sont refusés (403).

Erreurs en plus des habituelles :

StatutQuand
402Aucune carte enregistrée. Corps : {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"}
403Jeton en lecture seule ou à portée application, une limite d’équipe atteinte ("this team has reached its limit of 3 servers"), ou la limite de dépenses mensuelle serait dépassée
409Conflit : un nom de clé SSH avec une clé différente, un nom de bucket déjà utilisé, un bucket qui contient encore des objets, un conflit de CNAME, démarrer un serveur qui n’est pas arrêté
422Entrée invalide : taille inconnue, image inconnue, incompatibilité d’architecture entre l’image et la taille, nom DNS hors de votre zone, IP privée dans un enregistrement A
502Le cloud a rejeté la requête; le message en indique la raison
503Railyard Cloud n’est pas activé

Tailles : GET /api/cloud/sizes

{ "sizes": [
  { "name": "t4g.small", "vcpus": 2, "memory_gb": 2, "disk_gb": 20, "arch": "arm64",
    "hourly_cost": "0.023992", "markup_percent": "10.0", "hourly_price": "0.026391", "monthly_price": "19.27" }
] }

hourly_cost est ce que le serveur coûte à Railyard (machine, IPv4 publique et disque); hourly_price est ce que l’équipe paie : cost × (1 + markup_percent / 100).

Serveurs

curl -s -X POST https://app.railyard.run/api/cloud/ssh_keys \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d "{\"name\":\"laptop\",\"public_key\":\"$(cat ~/.ssh/id_ed25519.pub)\"}"

curl -s -X POST https://app.railyard.run/api/cloud/servers \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"name":"n8n-production","size":"t4g.small","image":"ubuntu-24.04","ssh_keys":["laptop"]}'
{ "server": { "id": 9, "name": "n8n-production", "status": "pending", "size": "t4g.small", "arch": "arm64",
  "image": "ubuntu-24.04", "disk_gb": 20, "public_ipv4": null, "private_ipv4": null, "public_ipv6": null,
  "ssh_keys": ["laptop"], "hourly_price": "0.026391", "failure": null, "created_at": "2026-10-10T12:00:00Z" } }

Interrogez GET /api/cloud/servers/9 toutes les quelques secondes jusqu’à ce que status soit running et que public_ipv4 soit défini (habituellement de 10 à 30 secondes), puis ssh ubuntu@<public_ipv4>. failed signifie que le lancement a été refusé; failure en indique la raison. image vaut ubuntu-24.04 ou le nom d’une de vos propres images construites pour la même architecture que la taille.

Images

curl -s -X POST https://app.railyard.run/api/cloud/servers/9/images \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"name":"n8n-base"}'

Interrogez GET /api/cloud/images/:id jusqu’à ce que status soit available (quelques minutes). Lancez de nouveaux serveurs à partir de celle-ci avec "image": "n8n-base". Une image dormante ne coûte que sa taille stockée (stored_gb), ce qui permet à une copie de secours d’un service d’attendre pour quelques cents par mois.

Adresses : une IPv4 statique qui survit au remplacement du serveur derrière elle.

curl -s -X POST https://app.railyard.run/api/cloud/addresses -H "Authorization: Bearer $RLY"
# {"address":{"id":3,"ip":"198.51.100.9","server_id":null,"created_at":"..."}}

curl -s -X POST https://app.railyard.run/api/cloud/addresses/3/assignment \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"server_id":9}'

Zone DNS : chaque équipe obtient sa propre zone, donc aucun domaine personnel n’est requis.

curl -s https://app.railyard.run/api/cloud/zone -H "Authorization: Bearer $RLY"
# {"zone":{"name":"acme.cloud.railyard.run"}}

curl -s -X PUT https://app.railyard.run/api/cloud/zone/records/A/n8n-production \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"values":["198.51.100.9"],"ttl":300}'
# {"record":{"id":11,"name":"n8n-production","fqdn":"n8n-production.acme.cloud.railyard.run","type":"A","values":["198.51.100.9"],"ttl":300,...}}
  • name est relatif à la zone (n8n-production) ou pleinement qualifié à l’intérieur de celle-ci; tout ce qui est hors de votre zone donne 422, et l’apex de la zone est réservé.
  • Types : A, AAAA, CNAME, TXT. TXT fonctionne pour le défi DNS-01 de certbot : PUT /api/cloud/zone/records/TXT/_acme-challenge.n8n-production.
  • Les valeurs A doivent être des adresses publiques. Un CNAME ne peut pas partager son nom avec d’autres enregistrements.
  • PUT remplace les valeurs de l’enregistrement; DELETE sur le même chemin le supprime (204).

Buckets

curl -s -X POST https://app.railyard.run/api/cloud/buckets \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" -d '{"name":"n8n-backups"}'
{ "bucket": { "id": 2, "name": "n8n-backups", "url": "s3:https://s3.us-east-2.amazonaws.com/rly-7-n8n-backups-ab12cd",
  "region": "us-east-2", "created_at": "2026-10-10T12:00:00Z", "access_key_id": "AKIA…", "secret_access_key": "…" } }

url est dans le format s3: de restic. La clé d’accès appartient à l’équipe et ne fonctionne que sur les buckets de l’équipe; chaque bucket que vous créez retourne la même clé, et la liste des buckets ne l’affiche jamais. DELETE exige que le bucket soit vide.

Utilisation : GET /api/cloud/usage

{ "usage": {
  "lines": [ { "description": "Server n8n-production (t4g.small)", "metric": "server_hours", "quantity": "1.0",
               "period_start": "2026-10-10T12:00:00Z", "cost_usd": "0.023992", "markup_percent": "10.0",
               "price_usd": "0.026391", "invoiced": false } ],
  "cost_usd": "0.023992", "price_usd": "0.026391", "our_cut_usd": "0.002399", "spend_limit_usd": "100.0" } }

L’utilisation est mesurée chaque heure par ressource : heures de serveur, trafic sortant, heures d’adresse statique, stockage d’image et stockage de bucket. Les cartes sont débitées quand le total non facturé dépasse un seuil ou au début de chaque mois.


Modèles

Des applications prêtes à l’emploi en un clic : le même catalogue qu’offre la page « deploy a template » du tableau de bord.

curl -s https://cp.example.com/api/templates -H "Authorization: Bearer $RLY"
{ "templates": [
  { "key": "n8n", "name": "n8n", "category": "automation", "verified": true,
    "addons": ["postgres"], "description": "Workflow automation" }
] }

Lancer : POST /api/templates/:key/launch

curl -s -X POST https://cp.example.com/api/templates/n8n/launch \
  -H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
  -d '{"server_id":9}'
{ "application_id": 42, "name": "n8n", "url": "https://n8n.apps.example.com", "deploy_id": 101 }

server_id est optionnel : si vous l’omettez, Railyard choisit le premier hôte d’application en ligne. 201 avec le premier déploiement déjà en file. 422 si l’équipe dépasse sa limite d’applications ou si les valeurs par défaut du modèle échouent à la validation.


Recettes rapides

Déployer à chaque push (CI) :

curl -fsS -X POST "$RAILYARD/api/applications/$APP_ID/deploys" \
  -H "Authorization: Bearer $RAILYARD_TOKEN" -H "Content-Type: application/json" \
  -d "{\"ref\":\"$GIT_SHA\"}"

Attendre la fin d’un déploiement :

id=$(curl -fsS -X POST "$RAILYARD/api/applications/$APP_ID/deploys" \
      -H "Authorization: Bearer $TOKEN" -d '{}' | jq .id)
while :; do
  s=$(curl -fsS "$RAILYARD/api/deploys/$id" -H "Authorization: Bearer $TOKEN" | jq -r .status)
  case $s in succeeded) exit 0;; failed) exit 1;; esac
  sleep 5
done

Synchroniser les variables d’environnement depuis un fichier (lignes KEY=value) :

jq -Rn '[inputs | select(test("=")) | split("=") | {(.[0]): (.[1:] | join("="))}] | add | {env: .}' .env.prod \
| curl -fsS -X PATCH "$RAILYARD/api/applications/$APP_ID/env" \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d @-

Pas encore dans l’API

Console interactive (un TTY via le RPC Exec), attacher un nouveau serveur, la gestion des modules complémentaires au-delà du tableau addons, créer ou modifier des règles de notification, des points de terminaison webhook ou des entrées de configuration (le listage est en lecture seule aujourd’hui), le fil d’activité. Tout ceci n’est offert que depuis le tableau de bord aujourd’hui; ouvrez un ticket si vous avez besoin de l’un de ces éléments par l’API.

MCP (agents IA)

POST /api/mcp est un serveur Model Context Protocol (JSON-RPC par HTTP en streaming), ce qui permet aux agents IA d’utiliser Railyard avec les mêmes jetons d’API.

  • Un jeton read peut lister les serveurs et les applications, afficher une application et lire ses logs.
  • Un jeton write peut aussi déployer, redémarrer et définir des variables d’environnement.
  • Supprimer une application ou retirer une variable d’environnement ne s’exécute jamais directement : cela crée une demande d’approbation. Un administrateur de l’équipe l’approuve ou la rejette sous Approvals (elle apparaît aussi dans le tableau de bord).

Claude Code :

claude mcp add --transport http railyard https://app.railyard.run/api/mcp --header "Authorization: Bearer rly_..."

Cursor (.cursor/mcp.json) :

{ "mcpServers": { "railyard": { "url": "https://app.railyard.run/api/mcp", "headers": { "Authorization": "Bearer rly_..." } } } }

Outils : list_servers, list_apps, get_app, app_logs, deploy_app, restart_app, set_env, unset_env (approbation), delete_app (approbation).