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/jsonen 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
| Scope | Puede |
|---|---|
read | todo GET |
write | todo 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:400solicitud incorrecta ·401sin autenticar ·403scope incorrecto ·404no encontrado (o no está en tu equipo) ·409conflicto (por ejemplo, ya hay un despliegue en curso) ·422falló la validación ·429lí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
limitaceptan1–100(por defecto30). 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
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/me | read | Token + info del equipo, límites, conteos |
| GET | /api/applications | read | Lista las apps |
| POST | /api/applications | write | Crea una app |
| GET | /api/applications/:id | read | Detalle de la app |
| PATCH | /api/applications/:id | write | Actualiza una app |
| DELETE | /api/applications/:id | write | Elimina una app (y sus despliegues/logs) |
| POST | /api/applications/:id/archive | write | Detiene el contenedor, conserva el registro |
| POST | /api/applications/:id/unarchive | write | Desarchiva |
| GET | /api/applications/:id/env | read | Lee las variables de entorno |
| PATCH | /api/applications/:id/env | write | Combina variables de entorno (un valor null elimina) |
| DELETE | /api/applications/:id/env?key=KEY | write | Elimina una variable de entorno |
| GET | /api/applications/:id/edge | read | Configuración de edge (misma forma que la sección edge: del manifest, consulta Reglas de edge) |
| PATCH | /api/applications/:id/edge | write | Combina la configuración de edge: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}} |
| GET | /api/applications/:id/certificates | read | Certificados subidos (nombres, hosts cubiertos, validez, fingerprint; nunca la clave) |
| POST | /api/applications/:id/certificates | write | Sube {"certificate":{"certificate_pem":"…","private_key_pem":"…"}} |
| DELETE | /api/applications/:id/certificates/:cert_id | write | Elimina uno (los certificados automáticos vuelven después del siguiente despliegue) |
| GET | /api/applications/:id/domains | read | Lista los dominios personalizados |
| POST | /api/applications/:id/domains | write | Agrega un dominio personalizado |
| DELETE | /api/applications/:id/domains/:domain_id | write | Elimina un dominio personalizado |
| GET | /api/applications/:id/deploys | read | Despliegues de una app |
| POST | /api/applications/:id/deploys | write | Dispara un despliegue ({"image": "registry/repo:tag"} despliega esa imagen prearmada una sola vez) |
| GET | /api/applications/:id/registry | write | El registry de build del equipo para deploy --local: repository, username, password, platform |
| POST | /api/applications/:id/tunnel | write | Abre 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/logs | read | Logs en vivo del contenedor (?tail=, ?process=) |
| GET | /api/applications/:id/processes | read | Estado por contenedor (para ps) |
| POST | /api/applications/:id/restart | write | Reinicia todos los contenedores, o uno con ?process= |
| GET | /api/applications/:id/process_types | read | Tipos de proceso con configuración de env, apagado y tamaño |
| PATCH | /api/applications/:id/process_types/:pt_id | write | Configura env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit |
| GET | /api/applications/:id/commands | read | Ejecuciones recientes de comandos de un solo uso |
| POST | /api/applications/:id/commands | write | Ejecuta un comando de un solo uso ({"command": "..."}) |
| GET | /api/applications/:id/commands/:cmd_id | read | Una ejecución: status, output, exit code |
| POST | /api/applications/:id/reindex | write | Reconstruye 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/clone | write | Clona la app en una copia de staging |
| POST | /api/applications/:id/promote | write | Promueve el release de una app de staging a producción |
| GET | /api/applications/:id/manifest | read | La app como railyard.yml (?secrets=true incluye los valores de las variables) |
| POST | /api/applications/:id/apply_manifest | write | Aplica un manifest ({"manifest": "<yaml text>"}) |
| POST | /api/applications/validate_manifest | read | Verifica 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_query | read / write | Ejecuta SQL ({"sql": "...", "write": false}); write: true necesita un token con scope de escritura |
| GET/POST | /api/applications/:id/database_backups | read / write | Lista copias de seguridad / toma una ahora |
| POST | /api/database_backups/:id/restore | write | Restaura una copia de seguridad |
| GET | /api/database_backups/:id/download | read | Enlace de descarga firmado |
| GET/POST | /api/applications/:id/volume_backups | read / write | Lista copias de seguridad / respalda un volumen ({"volume": "name"}) |
| POST | /api/volume_backups/:id/restore | write | Restaura una copia de seguridad de volumen |
| GET | /api/applications/:id/traffic | read | Solicitudes, p95, tasa de error y los endpoints más lentos (?range=1h|24h|7d|30d) |
| PATCH | /api/applications/:id/autoscale | write | Escala 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_move | write | Mueve 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_datastores | write | Apunta 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/:kind | write | Trae esos datos de vuelta a Railyard (?copy=true los copia de regreso) |
| GET | /api/applications/:id/addons/:kind/transfers | read | Exportaciones/importaciones recientes de un complemento (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), con enlaces de descarga |
| POST | /api/applications/:id/addons/:kind/transfers | write | Exporta los datos del complemento; con multipart file + confirm=<app name>, carga un archivo en él |
| GET | /api/applications/:id/storage/objects | read | Explora 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/objects | write | Sube un archivo (multipart file, prefix o key) |
| DELETE | /api/applications/:id/storage/object?key= | write | Elimina un archivo, o una carpeta cuando la key termina en / |
| PATCH | /api/applications/:id/storage/visibility | write | Hace una carpeta pública o privada ({"prefix":"avatars/","public":true}) |
| GET | /api/applications/:id/redis | read | Dónde vive el Redis de la app (db N compartida o propia), su límite y política, memoria usada |
| PATCH | /api/applications/:id/redis | write | {"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/deploys | read | Despliegues de todo el equipo (?application_id=, ?limit=) |
| GET | /api/deploys/:id | read | Detalle del despliegue |
| GET | /api/deploys/:id/logs | read | Líneas del log del despliegue (las más antiguas primero, ≤5000) |
| GET | /api/applications/:id/log_search | read | Líneas de log almacenadas: rango de tiempo, búsqueda, páginas con cursor (consulta Logs, métricas y alertas) |
| GET | /api/applications/:id/log_download | read | Líneas de log almacenadas para un rango, como texto |
| GET/PATCH/DELETE | /api/applications/:id/otel_export | read / write | Configuración de exportación de OpenTelemetry (consulta Logs, métricas y alertas) |
| GET/POST | /api/applications/:id/metric_alerts | read / write | Alertas de umbral de métricas y su historial |
| PATCH/DELETE | /api/applications/:id/metric_alerts/:alert_id | write | Cambia o elimina una alerta |
| GET | /api/applications/:id/job_queue | read | Estadísticas de la cola de jobs, jobs fallidos, historial (consulta Logs, métricas y alertas) |
| POST | /api/applications/:id/job_queue/failed/:job_id/retry | write | Reintenta un job fallido |
| DELETE | /api/applications/:id/job_queue/failed/:job_id | write | Descarta un job fallido |
| GET | /api/servers | read | La flota |
| GET | /api/servers/:id | read | Detalle del servidor + estados de proceso en vivo |
| POST | /api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_docker | write | Acciones del ciclo de vida del servidor |
| PATCH | /api/servers/:id/resize | write | Redimensiona en el proveedor ({"size": "..."}) |
| POST | /api/servers/:id/trust_ip | write | Desbanea 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_rules | read | Reglas de notificación de todo el equipo |
| GET | /api/webhook_endpoints | read | Endpoints de webhook de todo el equipo |
| GET | /api/config | read | Entradas 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(oserver_id) puede ser un id o hostname de servidor. Omítelo y la app cae en el primer servidor sin cordon del equipo.- Un
hostnameen 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 elgit_refde la app, que unrefnunca cambia. Eltriggerdel despliegue esrefy su camporefmuestra lo que usó. Los nombres con caracteres que las refs de git no pueden tener devuelven422. Se ignora para una appimage://.message(opcional) — una nota de despliegue (hasta 500 caracteres), devuelta comomessagey mostrada en el panel, la actividad, las notificaciones y los webhooksdeploy.*.at(opcional) — hora ISO 8601 para desplegar en lugar de ahora. Responde201constatus: "scheduled"yscheduled_for; la hora debe estar en el futuro y dentro de la ventana de despliegue (422si no). Cancélalo conPOST /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 responde409conoutside_window: trueywindow_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 agregarestored_config. Envía"restore_config": falsepara 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 constatus: "pending_approval"si la app requiere aprobación de despliegue (apruébalo en el panel).409si ya hay un despliegue activo o esperando aprobación para esa app.429si se alcanza el límite de despliegues por hora del equipo.422si la app está archivada.422conmissing_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": truepara 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.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/cloud/sizes | read | Tamaños de servidor con costo por hora, markup y precio |
| GET | /api/cloud/ssh_keys | read | Lista claves SSH |
| POST | /api/cloud/ssh_keys | write | Agrega una clave SSH (201; 200 si la misma clave ya existe) |
| DELETE | /api/cloud/ssh_keys/:id | write | Elimina una clave SSH |
| GET | /api/cloud/servers | read | Lista servidores activos |
| POST | /api/cloud/servers | write | Lanza un servidor (201, pending) |
| GET | /api/cloud/servers/:id | read | Detalle del servidor, actualizado al leer |
| DELETE | /api/cloud/servers/:id | write | Termina un servidor (200, terminated) |
| POST | /api/cloud/servers/:id/power | write | Inicia un servidor detenido |
| DELETE | /api/cloud/servers/:id/power | write | Detiene un servidor en ejecución |
| POST | /api/cloud/servers/:id/images | write | Toma un snapshot de un servidor como imagen (201, creating) |
| GET | /api/cloud/images | read | Lista imágenes (?name= filtra) |
| GET | /api/cloud/images/:id | read | Detalle de la imagen, actualizado al leer |
| DELETE | /api/cloud/images/:id | write | Elimina una imagen |
| GET | /api/cloud/addresses | read | Lista direcciones estáticas |
| POST | /api/cloud/addresses | write | Asigna una dirección estática |
| GET | /api/cloud/addresses/:id | read | Detalle de la dirección |
| DELETE | /api/cloud/addresses/:id | write | Libera una dirección estática |
| POST | /api/cloud/addresses/:id/assignment | write | La conecta a un servidor ({"server_id": 9}) |
| DELETE | /api/cloud/addresses/:id/assignment | write | La desconecta |
| GET | /api/cloud/zone | read | La zona DNS del equipo |
| GET | /api/cloud/zone/records | read | Lista registros |
| PUT | /api/cloud/zone/records/:type/:name | write | Crea o reemplaza un registro |
| DELETE | /api/cloud/zone/records/:type/:name | write | Elimina un registro |
| GET | /api/cloud/buckets | read | Lista buckets |
| POST | /api/cloud/buckets | write | Crea un bucket; la respuesta lleva la clave de storage del equipo |
| DELETE | /api/cloud/buckets/:id | write | Elimina un bucket (debe estar vacío) |
| GET | /api/cloud/usage | read | Lí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:
| Status | When |
|---|---|
402 | No hay tarjeta registrada. Body: {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"} |
403 | Token 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 |
409 | Conflicto: 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 |
422 | Entrada 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 |
502 | La nube rechazó la solicitud; el mensaje dice por qué |
503 | Railyard 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,...}}
namees relativo a la zona (n8n-production) o totalmente calificado dentro de ella; cualquier cosa fuera de tu zona es422, 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
Adeben ser direcciones públicas. Un CNAME no puede compartir su nombre con otros registros. PUTreemplaza los valores del registro;DELETEen 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).