API REST

API REST

La API JSON por equipo detrás de la CLI: apps, variables de entorno, despliegues, logs y servidores.

Una API JSON pequeña y acotada por equipo para manejar Railyard desde una CLI, un job de CI o un script — listar y crear apps, configurar variables de entorno, disparar despliegues, leer logs de despliegue, revisar la flota.

  • URL base — tu control plane, bajo /api. Compose local: http://localhost:3000/api
  • Formato — solo JSON. Envía Content-Type: application/json en las escrituras. Sin CSRF, sin sesión, sin cookies.
  • Todo está acotado a un equipo — el equipo al que pertenece el token.

Authentication

Cada solicitud necesita un bearer token:

Authorization: Bearer rly_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Obtén un token: panel → Team → API tokens → nómbralo, elige un scope, Create. La cadena rly_… se muestra una sola vez — cópiala en ese momento. Solo para admins.

Scopes

ScopePuede
readtodo GET
writetodo lo que puede read, más POST / PATCH / DELETE

Un token de solo lectura en un endpoint que modifica datos → 403 {"error":"this API token is read-only"}. Token faltante, inválido, revocado o expirado → 401 {"error":"missing or invalid API token"}.

Revócalo desde la misma página de Team; un token revocado o con expires_at pasado deja de funcionar inmediatamente.


Conventions

  • Errores — las respuestas que no son 2xx son { "error": "<message>" }. Códigos de estado: 400 solicitud incorrecta · 401 sin autenticar · 403 scope incorrecto · 404 no encontrado (o no está en tu equipo) · 409 conflicto (por ejemplo, ya hay un despliegue en curso) · 422 falló la validación · 429 límite de tasa.
  • Timestamps — ISO-8601 UTC (2026-09-02T14:03:11Z).
  • IDs — enteros, estables por equipo.
  • Paginación — los endpoints de listado que aceptan limit aceptan 1–100 (por defecto 30). Todavía no hay cursor; los más recientes primero cuando se ordena por tiempo.
  • Límite de tasa — la creación de despliegues está limitada por equipo por hora (GET /api/me → limits.deploys_per_hour); si se supera → 429.

Endpoints

MethodPathScopePurpose
GET/api/mereadToken + info del equipo, límites, conteos
GET/api/applicationsreadLista las apps
POST/api/applicationswriteCrea una app
GET/api/applications/:idreadDetalle de la app
PATCH/api/applications/:idwriteActualiza una app
DELETE/api/applications/:idwriteElimina una app (y sus despliegues/logs)
POST/api/applications/:id/archivewriteDetiene el contenedor, conserva el registro
POST/api/applications/:id/unarchivewriteDesarchiva
GET/api/applications/:id/envreadLee las variables de entorno
PATCH/api/applications/:id/envwriteCombina variables de entorno (un valor null elimina)
DELETE/api/applications/:id/env?key=KEYwriteElimina una variable de entorno
GET/api/applications/:id/edgereadConfiguración de edge (misma forma que la sección edge: del manifest, consulta Reglas de edge)
PATCH/api/applications/:id/edgewriteCombina la configuración de edge: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}}
GET/api/applications/:id/certificatesreadCertificados subidos (nombres, hosts cubiertos, validez, fingerprint; nunca la clave)
POST/api/applications/:id/certificateswriteSube {"certificate":{"certificate_pem":"…","private_key_pem":"…"}}
DELETE/api/applications/:id/certificates/:cert_idwriteElimina uno (los certificados automáticos vuelven después del siguiente despliegue)
GET/api/applications/:id/domainsreadLista los dominios personalizados
POST/api/applications/:id/domainswriteAgrega un dominio personalizado
DELETE/api/applications/:id/domains/:domain_idwriteElimina un dominio personalizado
GET/api/applications/:id/deploysreadDespliegues de una app
POST/api/applications/:id/deployswriteDispara un despliegue ({"image": "registry/repo:tag"} despliega esa imagen prearmada una sola vez)
GET/api/applications/:id/registrywriteEl registry de build del equipo para deploy --local: repository, username, password, platform
POST/api/applications/:id/tunnelwriteAbre un túnel ({"target": "web|db|redis|<process>", "local_port": 5432}): devuelve un ticket de 12 horas para el WebSocket TunnelChannel, el port remoto, y una url de localhost para bases de datos; se registra como app.tunnel_opened
GET/api/applications/:id/logsreadLogs en vivo del contenedor (?tail=, ?process=)
GET/api/applications/:id/processesreadEstado por contenedor (para ps)
POST/api/applications/:id/restartwriteReinicia todos los contenedores, o uno con ?process=
GET/api/applications/:id/process_typesreadTipos de proceso con configuración de env, apagado y tamaño
PATCH/api/applications/:id/process_types/:pt_idwriteConfigura env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit
GET/api/applications/:id/commandsreadEjecuciones recientes de comandos de un solo uso
POST/api/applications/:id/commandswriteEjecuta un comando de un solo uso ({"command": "..."})
GET/api/applications/:id/commands/:cmd_idreadUna ejecución: status, output, exit code
POST/api/applications/:id/reindexwriteReconstruye los índices de búsqueda: corre el search.reindex_command de la app en un contenedor de un solo uso (202; 422 sin un comando o sin un despliegue exitoso, 409 mientras uno está corriendo). Configura reindex_command / auto_reindex con PATCH /api/applications/:id; GET los muestra bajo search con la library detectada
POST/api/applications/:id/clonewriteClona la app en una copia de staging
POST/api/applications/:id/promotewritePromueve el release de una app de staging a producción
GET/api/applications/:id/manifestreadLa app como railyard.yml (?secrets=true incluye los valores de las variables)
POST/api/applications/:id/apply_manifestwriteAplica un manifest ({"manifest": "<yaml text>"})
POST/api/applications/validate_manifestreadVerifica un manifest en busca de errores sin aplicarlo — sin :id, funciona con el token de cualquier app ({"manifest": "<yaml text>"} → {"problems": [...]}, vacío cuando es válido)
POST/api/applications/:id/db_queryread / writeEjecuta SQL ({"sql": "...", "write": false}); write: true necesita un token con scope de escritura
GET/POST/api/applications/:id/database_backupsread / writeLista copias de seguridad / toma una ahora
POST/api/database_backups/:id/restorewriteRestaura una copia de seguridad
GET/api/database_backups/:id/downloadreadEnlace de descarga firmado
GET/POST/api/applications/:id/volume_backupsread / writeLista copias de seguridad / respalda un volumen ({"volume": "name"})
POST/api/volume_backups/:id/restorewriteRestaura una copia de seguridad de volumen
GET/api/applications/:id/trafficreadSolicitudes, p95, tasa de error y los endpoints más lentos (?range=1h|24h|7d|30d)
PATCH/api/applications/:id/autoscalewriteEscala las réplicas web según p95/CPU ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} lo desactiva
POST/api/applications/:id/data_movewriteMueve los datos de la app a un servidor de base de datos ({"database_server_id": 12}); 202 con lo que se copia
POST/api/applications/:id/external_datastoreswriteApunta database, redis o elasticsearch de la app a una URL externa ({"kind","url","copy"}); la URL se verifica primero (502 si no es alcanzable), 202 cuando queda en cola
DELETE/api/applications/:id/external_datastores/:kindwriteTrae esos datos de vuelta a Railyard (?copy=true los copia de regreso)
GET/api/applications/:id/addons/:kind/transfersreadExportaciones/importaciones recientes de un complemento (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), con enlaces de descarga
POST/api/applications/:id/addons/:kind/transferswriteExporta los datos del complemento; con multipart file + confirm=<app name>, carga un archivo en él
GET/api/applications/:id/storage/objectsreadExplora el bucket del complemento de storage (?prefix=, ?token=): carpetas, archivos con tamaño, bandera de público y un enlace firmado
POST/api/applications/:id/storage/objectswriteSube un archivo (multipart file, prefix o key)
DELETE/api/applications/:id/storage/object?key=writeElimina un archivo, o una carpeta cuando la key termina en /
PATCH/api/applications/:id/storage/visibilitywriteHace una carpeta pública o privada ({"prefix":"avatars/","public":true})
GET/api/applications/:id/redisreadDónde vive el Redis de la app (db N compartida o propia), su límite y política, memoria usada
PATCH/api/applications/:id/rediswrite{"dedicated":true,"maxmemory_mb":512,"policy":"allkeys-lru"} le da a la app su propio Redis (se copian las keys) o cambia su límite; {"dedicated":false} lo regresa
GET/api/deploysreadDespliegues de todo el equipo (?application_id=, ?limit=)
GET/api/deploys/:idreadDetalle del despliegue
GET/api/deploys/:id/logsreadLíneas del log del despliegue (las más antiguas primero, ≤5000)
GET/api/applications/:id/log_searchreadLíneas de log almacenadas: rango de tiempo, búsqueda, páginas con cursor (consulta Logs, métricas y alertas)
GET/api/applications/:id/log_downloadreadLíneas de log almacenadas para un rango, como texto
GET/PATCH/DELETE/api/applications/:id/otel_exportread / writeConfiguración de exportación de OpenTelemetry (consulta Logs, métricas y alertas)
GET/POST/api/applications/:id/metric_alertsread / writeAlertas de umbral de métricas y su historial
PATCH/DELETE/api/applications/:id/metric_alerts/:alert_idwriteCambia o elimina una alerta
GET/api/applications/:id/job_queuereadEstadísticas de la cola de jobs, jobs fallidos, historial (consulta Logs, métricas y alertas)
POST/api/applications/:id/job_queue/failed/:job_id/retrywriteReintenta un job fallido
DELETE/api/applications/:id/job_queue/failed/:job_idwriteDescarta un job fallido
GET/api/serversreadLa flota
GET/api/servers/:idreadDetalle del servidor + estados de proceso en vivo
POST/api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_dockerwriteAcciones del ciclo de vida del servidor
PATCH/api/servers/:id/resizewriteRedimensiona en el proveedor ({"size": "..."})
POST/api/servers/:id/trust_ipwriteDesbanea y confía en una IP ({"ip": "..."}) — ActiveProtect levanta el ban en ~1 min. Permite que Watchtower elimine un ban sin abrir el panel.
GET/api/notification_rulesreadReglas de notificación de todo el equipo
GET/api/webhook_endpointsreadEndpoints de webhook de todo el equipo
GET/api/configreadEntradas de configuración de todo el equipo

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

Objeto app (forma de lista):

{
  "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 agrega: 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[] (solo los nombres — los valores nunca se devuelven en el detalle de la app; usa el endpoint de env), domains[] {hostname, redirect, cert_state}.

Crear — 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"}
      }}'

El body es {"application": { … }}. Claves aceptadas: 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 (una línea name: command por proceso), scheduled_tasks_text (name | cron | command por línea), db_extensions, deploy_approval_required, strict_pushes (un git push cuyo build falla es rechazado; por defecto true), addons (array), env_vars (objeto). En una actualización, también: server_id.

  • server (o server_id) puede ser un id o hostname de servidor. Omítelo y la app cae en el primer servidor sin cordon del equipo.
  • Un hostname en blanco se deriva — <name>.<server app_domain>, si no, APP_DOMAIN_SUFFIX, si no, <name>.<server-ip>.sslip.io.
  • Crear no despliega. Sigue con POST …/deploys.
  • Si se supera el límite de apps del equipo → 422.

201 con el objeto detallado de la app. PATCH → 200. DELETE → 204.

Archivar / desarchivar — POST /api/applications/:id/archive detiene el contenedor en ejecución y pone archived: true; el registro, la configuración y el historial se conservan. unarchive quita la bandera (vuelve a desplegar para traerla de regreso).


Env vars — /api/applications/:id/env

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

# combinar (configura dos, elimina una)
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}}'

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

PATCH combina con el conjunto existente; un valor null elimina esa clave. Los tres devuelven el {"env": { … }} completo. Los cambios surten efecto en el siguiente despliegue.


Custom domains — /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 hace que el hostname responda 301 hacia el hostname principal de la app en lugar de servirlo. Apunta el dns_target mostrado en tu DNS y Caddy emite el certificado. DELETE …/domains/:domain_id → 204.


Despliegues

Objeto 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 agrega error, triggered_by (email), duration_seconds.

Un despliegue canary también tiene un objeto canary (null si no lo es):

"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 inicia el despliegue de promoción (202, promotion_deploy_id); DELETE /api/deploys/:id/canary lo revierte. Ambos necesitan un token de escritura y responden 409 una vez que el canary ya no está activo.

Estrategia de despliegue — PATCH /api/applications/:id/deploy_strategy con cualquiera de deploy_strategy (standard · canary), canary_percent (1–99), canary_steps_text (porcentajes ascendentes separados por comas, por ejemplo "5, 25, 50"; en blanco = un solo paso), canary_promote_after_minutes (1–1440, vacío = manual), canary_max_error_rate (0–100). GET /api/applications/:id muestra los mismos campos. Con pasos configurados, promover un paso inicia el siguiente como su propio canary en lugar de ir directo al 100%.

Redesplegar cuando cambia la imagen (solo apps de imagen, si no, 422) — PATCH /api/applications/:id/image_watch con enabled (true/false); POST /api/applications/:id/image_watch/check verifica el registry ahora (202). Ambos devuelven esto, y GET /api/applications/:id lo muestra como 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 }

Los registries llaman a POST /webhooks/image/<token> (sin token de API; cualquier body) para disparar una verificación.

Disparar — 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 (opcional) — rama, tag o SHA a desplegar una sola vez; por defecto usa el git_ref de la app, que un ref nunca cambia. El trigger del despliegue es ref y su campo ref muestra lo que usó. Los nombres con caracteres que las refs de git no pueden tener devuelven 422. Se ignora para una app image://.
  • message (opcional) — una nota de despliegue (hasta 500 caracteres), devuelta como message y mostrada en el panel, la actividad, las notificaciones y los webhooks deploy.*.
  • at (opcional) — hora ISO 8601 para desplegar en lugar de ahora. Responde 201 con status: "scheduled" y scheduled_for; la hora debe estar en el futuro y dentro de la ventana de despliegue (422 si no). Cancélalo con POST /api/deploys/:id/cancel (también cancela un despliegue en cola o en ejecución).
  • ignore_window (opcional) — despliega aunque la app esté fuera de su ventana de despliegue. Sin esto, tal despliegue responde 409 con outside_window: true y window_opens_at.
  • rollback_to (opcional) — id de un despliegue exitoso anterior. Vuelve a desplegar su commit y restaura las variables con las que corrió; la respuesta agrega restored_config. Envía "restore_config": false para revertir solo el código. Los releases desplegados antes de que Railyard guardara una copia de sus variables siempre revierten solo el código.
  • 202 Accepted + el objeto de despliegue cuando queda en cola y en ejecución.
  • 200 OK + el objeto con status: "pending_approval" si la app requiere aprobación de despliegue (apruébalo en el panel).
  • 409 si ya hay un despliegue activo o esperando aprobación para esa app.
  • 429 si se alcanza el límite de despliegues por hora del equipo.
  • 422 si la app está archivada.
  • 422 con missing_env (array de nombres) si la app nunca se ha desplegado con éxito y las credenciales que documenta su repositorio no están configuradas o todavía son placeholders. Configúralas por el endpoint de env, o envía "force": true para desplegar de todas formas — esos nombres no se vuelven a pedir.

Ventana de despliegue — 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"}'

Devuelve {days, start, end, time_zone, open_now, opens_at} (también bajo deploy_window en GET /api/applications/:id). "days": [] elimina la ventana. Los push, los webhooks de GitHub/GitLab y los push combinados fuera de la ventana se convierten en un solo despliegue scheduled que inicia cuando se abre.

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. Limitado a 5000 líneas, las más antiguas primero. Haz polling mientras status sea queued/running.

Listar — GET /api/deploys?application_id=11&limit=50 o GET /api/applications/11/deploys.


Runtime — logs, processes, commands, restart

La mitad programática de la consola de la página de la app. Todo esto llega al agente en la máquina a través de un canal mTLS.

Logs en vivo — GET /api/applications/:id/logs?tail=200&process=web

Lo que la app en ejecución está imprimiendo ahora — no la salida del build (eso es /api/deploys/:id/logs). tail va de 1 a 5000 (por defecto 200); process filtra a un proceso. 409 con {"error": "No running deployment"} cuando todavía no hay nada corriendo, o el agente no es alcanzable.

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

Todavía no hay server-push para seguir en vivo — railyard logs --follow vuelve a pedir en un intervalo.

Procesos — GET /api/applications/:id/processes

El último estado reportado de cada contenedor, actualizado en cada heartbeat del agente. Lista vacía = nada corriendo.

{ "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 · …

Restart — POST /api/applications/:id/restart[?process=web]

Reinicia todos los contenedores, o solo los de un proceso. 409 si nada está corriendo.

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

Tipos de proceso — 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 es small, medium, large (mapeado desde PROCESS_SIZE_*), custom (entonces envía cpu_limit y memory_limit) o en blanco para el límite de toda la app. cpus / memory son los límites efectivos después de ese fallback.

kill_timeout_seconds (1–300, en blanco para el valor por defecto) es cuánto tiempo tiene el proceso después de stop_signal (SIGTERM, SIGINT, SIGQUIT, en blanco para el propio de la imagen) antes de ser terminado. Los cambios aplican en el siguiente despliegue. Un valor inválido → 422.

Comandos de un solo uso — 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"}'

Corre dentro del contenedor web vía el RPC RunTask del agente. 202 Accepted con la fila; haz polling de GET /api/applications/:id/commands/:cmd_id para el status (queued → running → succeeded/failed), el output transmitido, y el exit_code. GET /api/applications/:id/commands lista las ejecuciones recientes.

La console interactiva (una TTY, RPC Exec) todavía no está en la API.


Servers (read-only)

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

Objeto 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 (heartbeat < 30 s) · stale (< 5 min) · offline. GET /api/servers/:id agrega apps[] (nombres) y processes[] {container, state, restarts, crash_looping}.

Poner en cordon, reiniciar, power-cycle, pruning y redimensionar un servidor existente son POST/PATCH en /api/servers/:id (consulta la tabla de arriba). Conectar un servidor nuevo es solo desde el panel por ahora.

Ventana de mantenimiento de datos — GET/PATCH /api/maintenance_window (equipo) y GET/PATCH /api/servers/:id/maintenance_window (un servidor; día en blanco = usar el del equipo). Campos: maintenance_day 0–6 (domingo = 0), maintenance_hour 0–23 UTC, maintenance_hours 1–12. Consulta Servidores de base de datos.

Upgrades de versión mayor de Postgres — GET/POST /api/servers/:id/postgres_upgrades (to_image; crear uno corre la verificación de compatibilidad), GET …/:id (con el log), PATCH …/:id (when=window para esperar la ventana de mantenimiento, si no, inicia ahora), POST …/:id/rollback, DELETE …/:id (elimina la versión anterior conservada, o descarta un plan que no inició).

Pooler de conexiones — GET/POST/DELETE /api/servers/:id/connection_pooler (PgBouncer, modo transacción, puerto 6432). Por app: PATCH /api/applications/:id/database con app_database[pooled]=true apunta DATABASE_URL al pooler a partir del siguiente despliegue y conserva DATABASE_DIRECT_URL para las migraciones.

Forks de base de datos — GET/POST /api/applications/:id/database_forks (target_server_id para una base de datos nueva o target_application_id para un entorno de staging/preview, source=live|backup, mask_sql opcional), GET …/:id (con la URL del fork una vez listo), DELETE …/:id (lo elimina).

Réplicas standby — GET/POST /api/servers/:id/database_replicas (replica_server_id), POST …/:id/promote, DELETE …/:id.


Cloud

Railyard Cloud convierte la cuenta de Railyard de un equipo en su propia nube pequeña: servidores, imágenes, direcciones estáticas, una zona DNS y buckets S3, cobrados al costo de la nube más un markup publicado. Está hecho para herramientas como AliaJS que esperan la API de un proveedor de nube, así que las formas siguen las de Hetzner: cada respuesta envuelve su objeto ({"server": {...}}, {"servers": [...]}). Debe haber una tarjeta registrada (agrégala en la página de Cloud), y los endpoints responden 503 hasta que el operador activa Railyard Cloud.

MethodPathScopePurpose
GET/api/cloud/sizesreadTamaños de servidor con costo por hora, markup y precio
GET/api/cloud/ssh_keysreadLista claves SSH
POST/api/cloud/ssh_keyswriteAgrega una clave SSH (201; 200 si la misma clave ya existe)
DELETE/api/cloud/ssh_keys/:idwriteElimina una clave SSH
GET/api/cloud/serversreadLista servidores activos
POST/api/cloud/serverswriteLanza un servidor (201, pending)
GET/api/cloud/servers/:idreadDetalle del servidor, actualizado al leer
DELETE/api/cloud/servers/:idwriteTermina un servidor (200, terminated)
POST/api/cloud/servers/:id/powerwriteInicia un servidor detenido
DELETE/api/cloud/servers/:id/powerwriteDetiene un servidor en ejecución
POST/api/cloud/servers/:id/imageswriteToma un snapshot de un servidor como imagen (201, creating)
GET/api/cloud/imagesreadLista imágenes (?name= filtra)
GET/api/cloud/images/:idreadDetalle de la imagen, actualizado al leer
DELETE/api/cloud/images/:idwriteElimina una imagen
GET/api/cloud/addressesreadLista direcciones estáticas
POST/api/cloud/addresseswriteAsigna una dirección estática
GET/api/cloud/addresses/:idreadDetalle de la dirección
DELETE/api/cloud/addresses/:idwriteLibera una dirección estática
POST/api/cloud/addresses/:id/assignmentwriteLa conecta a un servidor ({"server_id": 9})
DELETE/api/cloud/addresses/:id/assignmentwriteLa desconecta
GET/api/cloud/zonereadLa zona DNS del equipo
GET/api/cloud/zone/recordsreadLista registros
PUT/api/cloud/zone/records/:type/:namewriteCrea o reemplaza un registro
DELETE/api/cloud/zone/records/:type/:namewriteElimina un registro
GET/api/cloud/bucketsreadLista buckets
POST/api/cloud/bucketswriteCrea un bucket; la respuesta lleva la clave de storage del equipo
DELETE/api/cloud/buckets/:idwriteElimina un bucket (debe estar vacío)
GET/api/cloud/usagereadLíneas de uso del mes con costo, markup y precio

Los bodies de las solicitudes son JSON y pueden ser planos ({"name": "web"}) o envueltos ({"server": {"name": "web"}}). Los tokens con scope de app son rechazados (403).

Errores además de los habituales:

StatusWhen
402No hay tarjeta registrada. Body: {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"}
403Token de solo lectura o con scope de app, se alcanzó un límite del equipo ("this team has reached its limit of 3 servers"), o se superaría el límite de gasto mensual
409Conflicto: un nombre de clave SSH con una clave distinta, un nombre de bucket ya usado, un bucket que todavía tiene objetos, un choque de CNAME, iniciar un servidor que no está detenido
422Entrada inválida: tamaño desconocido, imagen desconocida, arquitectura de imagen/tamaño no coincide, nombre DNS fuera de tu zona, IP privada en un registro A
502La nube rechazó la solicitud; el mensaje dice por qué
503Railyard Cloud no está activado

Tamaños: 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 es lo que el servidor le cuesta a Railyard (máquina, IPv4 pública y disco); hourly_price es lo que paga el equipo: cost × (1 + markup_percent / 100).

Servidores

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" } }

Haz polling de GET /api/cloud/servers/9 cada par de segundos hasta que status sea running y public_ipv4 esté configurado (normalmente 10 a 30 segundos), luego ssh ubuntu@<public_ipv4>. failed significa que el lanzamiento fue rechazado; failure dice por qué. image es ubuntu-24.04 o el nombre de una de tus propias imágenes construida para la misma arquitectura que el tamaño.

Imágenes

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"}'

Haz polling de GET /api/cloud/images/:id hasta que status sea available (unos minutos). Lanza servidores nuevos desde ella con "image": "n8n-base". Una imagen inactiva solo cuesta su tamaño almacenado (stored_gb), que es cómo una copia standby de un servicio puede esperar por centavos al mes.

Direcciones: una IPv4 estática que sobrevive al reemplazo del servidor detrás de ella.

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}'

Zona DNS: cada equipo obtiene su propia zona, así que no necesitas tu propio dominio.

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 es relativo a la zona (n8n-production) o totalmente calificado dentro de ella; cualquier cosa fuera de tu zona es 422, y el ápice de la zona está reservado.
  • Tipos: A, AAAA, CNAME, TXT. TXT funciona para el challenge DNS-01 de certbot: PUT /api/cloud/zone/records/TXT/_acme-challenge.n8n-production.
  • Los valores A deben ser direcciones públicas. Un CNAME no puede compartir su nombre con otros registros.
  • PUT reemplaza los valores del registro; DELETE en la misma ruta lo elimina (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á en la forma s3: de restic. La access key le pertenece al equipo y solo funciona en los buckets del equipo; cada bucket que creas devuelve la misma key, y listar buckets nunca la muestra. DELETE necesita que el bucket esté vacío.

Uso: 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", "daily_price_usd": "2.21",
  "spend_limit_usd": "100.0" } }

El uso se mide cada hora por recurso: horas de servidor, tráfico de salida, horas de dirección estática, almacenamiento de imágenes y de buckets. daily_price_usd es lo que costaron las últimas 24 horas medidas. Las tarjetas se cobran cuando el total no facturado supera un umbral o al inicio de cada mes.


Templates

Apps prearmadas de un clic — el mismo catálogo que ofrece la página de “desplegar una plantilla” del panel.

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" }
] }

Lanzar — 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 es opcional — omítelo y Railyard elige el primer host de apps en línea. 201 con el primer despliegue ya en cola. 422 si el equipo supera su límite de apps o los valores por defecto de la plantilla fallan la validación.


Quick recipes

Desplegar en cada 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\"}"

Esperar a que termine un despliegue:

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

Sincronizar env desde un archivo (líneas 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 @-

Not in the API yet

Consola interactiva (una TTY sobre el RPC Exec), conectar un servidor nuevo, administración de complementos más allá del array addons, crear o editar reglas de notificación / endpoints de webhook / entradas de configuración (hoy el listado es de solo lectura), feed de actividad. Hoy esto es solo desde el panel; abre un issue si necesitas alguno por la API.

MCP (AI agents)

POST /api/mcp es un servidor de Model Context Protocol (JSON-RPC sobre streamable HTTP), así que los agentes de IA pueden usar Railyard con los mismos tokens de API.

  • Un token de read puede listar servidores y apps, mostrar una app y leer sus logs.
  • Un token de write también puede desplegar, reiniciar y configurar variables de entorno.
  • Eliminar una app o quitar una variable de entorno nunca corre directamente: crea una solicitud de aprobación. Un admin del equipo la aprueba o rechaza en Approvals (también se muestra en el panel).

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_..." } } } }

Herramientas: list_servers, list_apps, get_app, app_logs, deploy_app, restart_app, set_env, unset_env (aprobación), delete_app (aprobación).