API REST

API REST

A API JSON com escopo por equipe por trás da CLI: aplicações, env, deploys, logs e servidores.

Uma API JSON pequena, com escopo por equipe, para operar o Railyard a partir de uma CLI, de um job de CI ou de um script — listar e criar aplicações, definir variáveis de ambiente, disparar deploys, ler logs de deploy, verificar a frota de servidores.

  • URL base — seu control plane, em /api. Compose local: http://localhost:3000/api
  • Formato — só JSON. Envie Content-Type: application/json nas escritas. Sem CSRF, sem sessão, sem cookies.
  • Tudo tem escopo em uma única equipe — a equipe à qual o token pertence.

Autenticação

Toda requisição precisa de um bearer token:

Authorization: Bearer rly_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Para gerar um token: painel → Equipe → Tokens de API → dê um nome, escolha um escopo, Criar. A string rly_… aparece uma única vez — copie nesse momento. Só admins podem gerar.

Escopos

EscopoPode
readtodo GET
writetudo que read pode, mais POST / PATCH / DELETE

Um token somente leitura em um endpoint de escrita → 403 {"error":"this API token is read-only"}. Token ausente / inválido / revogado / expirado → 401 {"error":"missing or invalid API token"}.

Revogue na mesma página da Equipe; um token revogado ou com expires_at no passado para de funcionar imediatamente.


Convenções

  • Erros — respostas fora da faixa 2xx vêm como { "error": "<message>" }. Códigos de status: 400 requisição inválida · 401 não autenticado · 403 escopo incorreto · 404 não encontrado (ou fora da sua equipe) · 409 conflito (por exemplo, um deploy já está em execução) · 422 falha de validação · 429 limite de taxa.
  • Timestamps — ISO-8601 UTC (2026-09-02T14:03:11Z).
  • IDs — inteiros, estáveis por equipe.
  • Paginação — endpoints de listagem que aceitam limit recebem de 1 a 100 (padrão 30). Ainda sem cursor; do mais novo para o mais antigo quando ordenado por tempo.
  • Limite de taxa — a criação de deploys tem um limite por equipe por hora (GET /api/me → limits.deploys_per_hour); acima disso → 429.

Endpoints

MétodoCaminhoEscopoFinalidade
GET/api/mereadInformações do token e da equipe, limites, contagens
GET/api/applicationsreadListar aplicações
POST/api/applicationswriteCriar uma aplicação
GET/api/applications/:idreadDetalhe da aplicação
PATCH/api/applications/:idwriteAtualizar uma aplicação
DELETE/api/applications/:idwriteExcluir uma aplicação (e seus deploys/logs)
POST/api/applications/:id/archivewriteParar o container, mantendo o registro
POST/api/applications/:id/unarchivewriteDesarquivar
GET/api/applications/:id/envreadLer variáveis de ambiente
PATCH/api/applications/:id/envwriteMesclar variáveis de ambiente (valor null exclui)
DELETE/api/applications/:id/env?key=KEYwriteExcluir uma variável de ambiente
GET/api/applications/:id/edgereadConfigurações de edge (mesmo formato da seção edge: do manifest, veja Regras de edge)
PATCH/api/applications/:id/edgewriteMesclar configurações de edge: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}}
GET/api/applications/:id/certificatesreadCertificados enviados (nomes, hosts cobertos, validade, fingerprint; nunca a chave)
POST/api/applications/:id/certificateswriteEnviar {"certificate":{"certificate_pem":"…","private_key_pem":"…"}}
DELETE/api/applications/:id/certificates/:cert_idwriteRemover um certificado (certificados automáticos a partir do próximo deploy)
GET/api/applications/:id/domainsreadListar domínios personalizados
POST/api/applications/:id/domainswriteAdicionar um domínio personalizado
DELETE/api/applications/:id/domains/:domain_idwriteRemover um domínio personalizado
GET/api/applications/:id/deploysreadDeploys de uma aplicação
POST/api/applications/:id/deployswriteDisparar um deploy ({"image": "registry/repo:tag"} faz deploy dessa imagem já buildada uma única vez)
GET/api/applications/:id/registrywriteO registro de build da equipe para o deploy --local: repository, username, password, platform
POST/api/applications/:id/tunnelwriteAbrir um túnel ({"target": "web|db|redis|<process>", "local_port": 5432}): retorna um ticket de 12 horas para o WebSocket do TunnelChannel, a port remota, e uma url em localhost para bancos de dados; registrado como app.tunnel_opened
GET/api/applications/:id/logsreadLogs do container em execução (?tail=, ?process=)
GET/api/applications/:id/processesreadEstado de cada container (para o ps)
POST/api/applications/:id/restartwriteReiniciar todos os containers, ou só um com ?process=
GET/api/applications/:id/process_typesreadTipos de processo com variáveis de ambiente, encerramento e configurações de tamanho
PATCH/api/applications/:id/process_types/:pt_idwriteDefinir env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit
GET/api/applications/:id/commandsreadExecuções recentes de comandos avulsos
POST/api/applications/:id/commandswriteRodar um comando avulso ({"command": "..."})
GET/api/applications/:id/commands/:cmd_idreadUma execução: status, saída, código de saída
POST/api/applications/:id/reindexwriteReconstruir os índices de busca: roda o search.reindex_command da aplicação em um container avulso (202; 422 sem um comando ou um deploy bem-sucedido, 409 enquanto um já está rodando). Defina reindex_command / auto_reindex com PATCH /api/applications/:id; o GET mostra em search, junto com a library detectada
POST/api/applications/:id/clonewriteClonar a aplicação em uma cópia de staging
POST/api/applications/:id/promotewritePromover a release de uma cópia de staging para produção
GET/api/applications/:id/manifestreadA aplicação como railyard.yml (?secrets=true inclui os valores das variáveis)
POST/api/applications/:id/apply_manifestwriteAplicar um manifest ({"manifest": "<yaml text>"})
POST/api/applications/validate_manifestreadVerificar um manifest em busca de erros sem aplicá-lo — sem :id, o token de qualquer aplicação funciona ({"manifest": "<yaml text>"} → {"problems": [...]}, vazio quando é válido)
POST/api/applications/:id/db_queryread / writeRodar SQL ({"sql": "...", "write": false}); write: true precisa de um token com escopo de escrita
GET/POST/api/applications/:id/database_backupsread / writeListar backups / tirar um agora
POST/api/database_backups/:id/restorewriteRestaurar um backup
GET/api/database_backups/:id/downloadreadLink de download assinado
GET/POST/api/applications/:id/volume_backupsread / writeListar backups / fazer backup de um volume ({"volume": "name"})
POST/api/volume_backups/:id/restorewriteRestaurar um backup de volume
GET/api/applications/:id/trafficreadRequisições, p95, taxa de erro e endpoints mais lentos (?range=1h|24h|7d|30d)
PATCH/api/applications/:id/autoscalewriteEscalar réplicas web por p95/CPU ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} desativa
POST/api/applications/:id/data_movewriteMover os dados da aplicação para um servidor de banco de dados ({"database_server_id": 12}); 202 com o que será copiado
POST/api/applications/:id/external_datastoreswriteApontar o database, redis ou elasticsearch da aplicação para uma URL externa ({"kind","url","copy"}); a URL é verificada antes (502 se inacessível), 202 quando enfileirado
DELETE/api/applications/:id/external_datastores/:kindwriteTrazer esses dados de volta para o Railyard (?copy=true copia os dados de volta)
GET/api/applications/:id/addons/:kind/transfersreadExportações/importações recentes de um add-on (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), com links de download
POST/api/applications/:id/addons/:kind/transferswriteExportar os dados do add-on; com file multipart + confirm=<app name>, carregar um arquivo nele
GET/api/applications/:id/storage/objectsreadNavegar no bucket do add-on de storage (?prefix=, ?token=): pastas, arquivos com tamanho, indicador de público e um link assinado
POST/api/applications/:id/storage/objectswriteEnviar um arquivo (file multipart, prefix ou key)
DELETE/api/applications/:id/storage/object?key=writeExcluir um arquivo, ou uma pasta quando a key termina em /
PATCH/api/applications/:id/storage/visibilitywriteTornar uma pasta pública ou privada ({"prefix":"avatars/","public":true})
GET/api/applications/:id/redisreadOnde o Redis da aplicação vive (db N compartilhado ou próprio), seu limite e política, memória usada
PATCH/api/applications/:id/rediswrite{"dedicated":true,"maxmemory_mb":512,"policy":"allkeys-lru"} dá à aplicação seu próprio Redis (chaves copiadas) ou muda o limite; {"dedicated":false} volta ao compartilhado
GET/api/deploysreadDeploys de toda a equipe (?application_id=, ?limit=)
GET/api/deploys/:idreadDetalhe do deploy
GET/api/deploys/:id/logsreadLinhas do log do deploy (mais antigas primeiro, ≤5000)
GET/api/applications/:id/log_searchreadLinhas de log armazenadas: intervalo de tempo, busca, páginas com cursor (veja Logs, métricas e alertas)
GET/api/applications/:id/log_downloadreadLinhas de log armazenadas de um intervalo, como texto
GET/PATCH/DELETE/api/applications/:id/otel_exportread / writeConfigurações de exportação OpenTelemetry (veja Logs, métricas e alertas)
GET/POST/api/applications/:id/metric_alertsread / writeAlertas de limite de métricas e seu histórico
PATCH/DELETE/api/applications/:id/metric_alerts/:alert_idwriteAlterar ou remover um alerta
GET/api/applications/:id/job_queuereadEstatísticas da fila de jobs, jobs com falha, histórico (veja Logs, métricas e alertas)
POST/api/applications/:id/job_queue/failed/:job_id/retrywriteTentar de novo um job com falha
DELETE/api/applications/:id/job_queue/failed/:job_idwriteDescartar um job com falha
GET/api/serversreadA frota de servidores
GET/api/servers/:idreadDetalhe do servidor + estado dos processos em tempo real
POST/api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_dockerwriteAções do ciclo de vida do servidor
PATCH/api/servers/:id/resizewriteRedimensionar no provedor ({"size": "..."})
POST/api/servers/:id/trust_ipwriteDesbanir e confiar em um IP ({"ip": "..."}) — o ActiveProtect remove o banimento em ~1 min. Permite que o Watchtower limpe um banimento sem abrir o painel.
GET/api/notification_rulesreadRegras de notificação de toda a equipe
GET/api/webhook_endpointsreadEndpoints de webhook de toda a equipe
GET/api/configreadEntradas de configuração de toda a equipe

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

Aplicações

Objeto app (na listagem):

{
  "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 adiciona: 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[] (só os nomes — os valores nunca são retornados no detalhe da aplicação; use o endpoint de env), domains[] {hostname, redirect, cert_state}.

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

O corpo é {"application": { … }}. Chaves aceitas: 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 (um name: command por linha), scheduled_tasks_text (name | cron | command por linha), db_extensions, deploy_approval_required, strict_pushes (um git push cujo build falha é rejeitado; true por padrão), addons (array), env_vars (objeto). Em atualizações, também: server_id.

  • server (ou server_id) pode ser um id ou hostname de servidor. Se omitido, a aplicação vai para o primeiro servidor sem cordon da equipe.
  • Um hostname em branco é derivado — <name>.<server app_domain>, senão APP_DOMAIN_SUFFIX, senão <name>.<server-ip>.sslip.io.
  • Criar não faz deploy. Siga com POST …/deploys.
  • Acima do limite de aplicações da equipe → 422.

201 com o objeto detalhado da aplicação. PATCH → 200. DELETE → 204.

Arquivar / desarquivar — POST /api/applications/:id/archive para o container em execução e define archived: true; o registro, a configuração e o histórico permanecem. unarchive remove a marcação (faça o deploy de novo para trazer a aplicação de volta).


Variáveis de ambiente — /api/applications/:id/env

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

# mesclar (define duas, exclui uma)
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}}'

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

PATCH mescla com o conjunto existente; um valor null remove essa chave. As três chamadas retornam o {"env": { … }} completo. As mudanças valem a partir do próximo deploy.


Domínios personalizados — /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 faz o hostname responder com 301 para o hostname principal da aplicação, em vez de servi-lo. Aponte o dns_target mostrado no seu DNS e o Caddy emite o certificado. DELETE …/domains/:domain_id → 204.


Deploys

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 adiciona error, triggered_by (e-mail), duration_seconds.

Um deploy canary também tem um objeto canary (null nos demais casos):

"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 o deploy de promoção (202, promotion_deploy_id); DELETE /api/deploys/:id/canary faz o rollback. As duas precisam de um token de escrita e respondem 409 quando o canary já não está mais ativo.

Estratégia de deploy — PATCH /api/applications/:id/deploy_strategy com qualquer combinação de deploy_strategy (standard · canary), canary_percent (1–99), canary_steps_text (porcentagens crescentes separadas por vírgula, por exemplo "5, 25, 50"; em branco = uma etapa), canary_promote_after_minutes (1–1440, vazio = manual), canary_max_error_rate (0–100). GET /api/applications/:id mostra os mesmos campos. Com etapas definidas, promover uma etapa inicia a próxima como seu próprio canary, em vez de ir direto para 100%.

Fazer deploy de novo quando a imagem mudar (só aplicações de imagem, senão 422) — PATCH /api/applications/:id/image_watch com enabled (true/false); POST /api/applications/:id/image_watch/check verifica o registro agora (202). As duas retornam, e GET /api/applications/:id mostra 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 }

Registros chamam POST /webhooks/image/<token> (sem token de API; qualquer corpo) para disparar uma verificação.

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) — branch, tag ou SHA para fazer deploy uma única vez; por padrão usa o git_ref da aplicação, que um ref nunca altera. O trigger do deploy é ref e seu campo ref mostra o que foi usado. Nomes com caracteres que uma ref de git não aceita recebem 422. Ignorado em uma aplicação image://.
  • message (opcional) — uma nota de deploy (até 500 caracteres), retornada como message e exibida no painel, na atividade, nas notificações e nos webhooks deploy.*.
  • at (opcional) — horário ISO 8601 para fazer o deploy, em vez de agora. Responde 201 com status: "scheduled" e scheduled_for; o horário precisa estar no futuro e dentro da janela de deploy (senão 422). Cancele com POST /api/deploys/:id/cancel (também cancela um deploy na fila ou em execução).
  • ignore_window (opcional) — faz o deploy mesmo que a aplicação esteja fora da janela de deploy. Sem isso, esse deploy responde 409 com outside_window: true e window_opens_at.
  • rollback_to (opcional) — id de um deploy anterior bem-sucedido. Refaz o deploy do commit dele e recupera as variáveis com que ele rodava; a resposta adiciona restored_config. Envie "restore_config": false para reverter só o código. Releases publicadas antes de o Railyard guardar uma cópia das variáveis sempre revertem apenas o código.
  • 202 Accepted + o objeto do deploy quando ele é colocado na fila e começa a rodar.
  • 200 OK + o objeto com status: "pending_approval" se a aplicação exige aprovação de deploy (aprove no painel).
  • 409 se já existe um deploy ativo ou aguardando aprovação para essa aplicação.
  • 429 se o limite de deploys por hora da equipe foi atingido.
  • 422 se a aplicação está arquivada.
  • 422 com missing_env (lista de nomes) se a aplicação nunca fez um deploy bem-sucedido e credenciais documentadas pelo repositório não foram definidas ou ainda são placeholders. Defina-as pelo endpoint de env, ou envie "force": true para fazer o deploy mesmo assim — esses nomes não são pedidos de novo depois.

Janela de deploy — 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"}'

Retorna {days, start, end, time_zone, open_now, opens_at} (também em deploy_window dentro de GET /api/applications/:id). "days": [] remove a janela. Pushes, webhooks do GitHub/GitLab e pushes agrupados fora da janela se tornam um único deploy scheduled que começa quando ela abrir.

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 linhas, das mais antigas para as mais novas. Consulte repetidamente enquanto status for queued/running.

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


Runtime — logs, processos, comandos, restart

A metade programática do console da página da aplicação. Tudo aqui passa pelo agente na máquina, por um canal mTLS.

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

O que a aplicação em execução está imprimindo agora — não a saída do build (que é /api/deploys/:id/logs). tail vai de 1 a 5000 (padrão 200); process filtra para um processo. 409 com {"error": "No running deployment"} quando nada está no ar ainda, ou o agente está inacessível.

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

Ainda não existe acompanhamento por push do servidor — o railyard logs --follow repete a requisição em intervalos.

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

Último estado reportado de cada container, atualizado em cada heartbeat do agente. Lista vazia = nada em execução.

{ "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 os containers, ou só o de um processo. 409 se nada está em execução.

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

Tipos de processo — 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 é small, medium, large (mapeado a partir de PROCESS_SIZE_*), custom (nesse caso envie cpu_limit e memory_limit) ou vazio para o limite geral da aplicação. cpus / memory são os limites efetivos depois desse fallback.

kill_timeout_seconds (1–300, vazio para o padrão) é quanto tempo o processo tem depois do stop_signal (SIGTERM, SIGINT, SIGQUIT, vazio para o da própria imagem) antes de ser encerrado à força. As mudanças valem a partir do próximo deploy. Um valor inválido → 422.

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

Roda dentro do container web pela RPC RunTask do agente. 202 Accepted com o registro; consulte GET /api/applications/:id/commands/:cmd_id para o status (queued → running → succeeded/failed), a output transmitida, e o exit_code. GET /api/applications/:id/commands lista as execuções recentes.

Um console interativo (um TTY, RPC Exec) ainda não está na API.


Servidores (somente leitura)

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 adiciona apps[] (nomes) e processes[] {container, state, restarts, crash_looping}.

Colocar em cordon, reiniciar, desligar e religar, limpar e redimensionar um servidor existente são POST/PATCH em /api/servers/:id (veja a tabela acima). Conectar um servidor novo ainda só é possível pelo painel.

Janela de manutenção de dados — GET/PATCH /api/maintenance_window (equipe) e GET/PATCH /api/servers/:id/maintenance_window (um servidor; dia em branco = usa o da equipe). Campos: maintenance_day 0–6 (domingo = 0), maintenance_hour 0–23 UTC, maintenance_hours 1–12. Veja Servidores de banco de dados.

Upgrades major do Postgres — GET/POST /api/servers/:id/postgres_upgrades (to_image; criar um roda a verificação de compatibilidade), GET …/:id (com o log), PATCH …/:id (when=window para esperar a janela de manutenção, senão começa agora), POST …/:id/rollback, DELETE …/:id (remove a versão antiga mantida, ou descarta um plano que ainda não começou).

Pooler de conexões — GET/POST/DELETE /api/servers/:id/connection_pooler (PgBouncer, modo transaction, porta 6432). Por aplicação: PATCH /api/applications/:id/database com app_database[pooled]=true aponta a DATABASE_URL para o pooler a partir do próximo deploy e mantém a DATABASE_DIRECT_URL para migrations.

Forks de banco de dados — GET/POST /api/applications/:id/database_forks (target_server_id para um banco novo, ou target_application_id para um ambiente de staging/preview, source=live|backup, mask_sql opcional), GET …/:id (com a URL do fork, quando pronto), DELETE …/:id (remove o fork).

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


Cloud

O Railyard Cloud transforma a conta Railyard de uma equipe na própria nuvem pequena dela: servidores, imagens, endereços fixos, uma zona de DNS e buckets S3, cobrados pelo custo de nuvem mais uma margem publicada. É feito para ferramentas como a AliaJS, que esperam a API de um provedor de nuvem, então os formatos seguem os da Hetzner: toda resposta envolve seu objeto ({"server": {...}}, {"servers": [...]}). É preciso ter um cartão cadastrado (adicione na página Cloud), e os endpoints respondem 503 até o operador ativar o Railyard Cloud.

MétodoCaminhoEscopoFinalidade
GET/api/cloud/sizesreadTamanhos de servidor com custo por hora, margem e preço
GET/api/cloud/ssh_keysreadListar chaves SSH
POST/api/cloud/ssh_keyswriteAdicionar uma chave SSH (201; 200 se a mesma chave já existir)
DELETE/api/cloud/ssh_keys/:idwriteRemover uma chave SSH
GET/api/cloud/serversreadListar servidores ativos
POST/api/cloud/serverswriteLançar um servidor (201, pending)
GET/api/cloud/servers/:idreadDetalhe do servidor, atualizado na leitura
DELETE/api/cloud/servers/:idwriteEncerrar um servidor (200, terminated)
POST/api/cloud/servers/:id/powerwriteIniciar um servidor parado
DELETE/api/cloud/servers/:id/powerwriteParar um servidor em execução
POST/api/cloud/servers/:id/imageswriteTirar um snapshot do servidor em uma imagem (201, creating)
GET/api/cloud/imagesreadListar imagens (?name= filtra)
GET/api/cloud/images/:idreadDetalhe da imagem, atualizado na leitura
DELETE/api/cloud/images/:idwriteExcluir uma imagem
GET/api/cloud/addressesreadListar endereços fixos
POST/api/cloud/addresseswriteAlocar um endereço fixo
GET/api/cloud/addresses/:idreadDetalhe do endereço
DELETE/api/cloud/addresses/:idwriteLiberar um endereço fixo
POST/api/cloud/addresses/:id/assignmentwriteAnexar a um servidor ({"server_id": 9})
DELETE/api/cloud/addresses/:id/assignmentwriteDesanexar
GET/api/cloud/zonereadA zona de DNS da equipe
GET/api/cloud/zone/recordsreadListar registros
PUT/api/cloud/zone/records/:type/:namewriteCriar ou substituir um registro
DELETE/api/cloud/zone/records/:type/:namewriteRemover um registro
GET/api/cloud/bucketsreadListar buckets
POST/api/cloud/bucketswriteCriar um bucket; a resposta traz a chave de storage da equipe
DELETE/api/cloud/buckets/:idwriteExcluir um bucket (precisa estar vazio)
GET/api/cloud/usagereadLinhas de uso deste mês, com custo, margem e preço

Os corpos das requisições são JSON e podem ser diretos ({"name": "web"}) ou envolvidos ({"server": {"name": "web"}}). Tokens com escopo de aplicação são recusados (403).

Erros além dos usuais:

StatusQuando
402Sem cartão cadastrado. Corpo: {"error": "add a card before using Railyard Cloud", "card_url": "https://app.railyard.run/cloud/card/new"}
403Token somente leitura ou com escopo de aplicação, um limite da equipe atingido ("this team has reached its limit of 3 servers"), ou o limite de gasto mensal seria superado
409Conflito: um nome de chave SSH com uma chave diferente, um nome de bucket já em uso, um bucket que ainda tem objetos, um conflito de CNAME, iniciar um servidor que não está parado
422Entrada inválida: tamanho desconhecido, imagem desconhecida, incompatibilidade de arquitetura entre imagem/tamanho, nome de DNS fora da sua zona, IP privado em um registro A
502A nuvem rejeitou a requisição; a mensagem explica o motivo
503O Railyard Cloud não está ativado

Tamanhos: 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 é o que o servidor custa para o Railyard (máquina, IPv4 público e disco); hourly_price é o que a equipe paga: custo × (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" } }

Consulte GET /api/cloud/servers/9 a cada poucos segundos até o status ser running e public_ipv4 estar definido (geralmente de 10 a 30 segundos), depois ssh ubuntu@<public_ipv4>. failed significa que o lançamento foi recusado; failure explica o motivo. image é ubuntu-24.04 ou o nome de uma das suas próprias imagens criadas para a mesma arquitetura do tamanho escolhido.

Imagens

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

Consulte GET /api/cloud/images/:id até o status ser available (alguns minutos). Lance servidores novos a partir dela com "image": "n8n-base". Uma imagem adormecida custa só o seu tamanho armazenado (stored_gb), o que permite que uma cópia standby de um serviço espere por centavos por mês.

Endereços: um IPv4 fixo que sobrevive à troca do servidor por trás dele.

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 de DNS: toda equipe recebe sua própria zona, então você não precisa de um domínio próprio.

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 é relativo à zona (n8n-production) ou totalmente qualificado dentro dela; qualquer coisa fora da sua zona recebe 422, e o ápice da zona é reservado.
  • Tipos: A, AAAA, CNAME, TXT. TXT funciona para o desafio DNS-01 do certbot: PUT /api/cloud/zone/records/TXT/_acme-challenge.n8n-production.
  • Valores de A precisam ser endereços públicos. Um CNAME não pode compartilhar o nome com outros registros.
  • PUT substitui os valores do registro; DELETE no mesmo caminho o remove (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á no formato s3: do restic. A access key pertence à equipe e só funciona nos buckets da equipe; todo bucket que você cria retorna a mesma chave, e listar os buckets nunca mostra ela. DELETE exige o bucket vazio.

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

O uso é medido a cada hora por recurso: horas de servidor, tráfego de saída, horas de endereço fixo, armazenamento de imagem e de bucket. daily_price_usd é o custo das últimas 24 horas medidas. Os cartões são cobrados quando o total não faturado passa de um limite, ou no início de cada mês.


Templates

Aplicações prontas de um clique — o mesmo catálogo que a página “fazer deploy de um template” do painel oferece.

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

Lançar — 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 é opcional — se omitido, o Railyard escolhe o primeiro servidor de aplicação online. 201 com o primeiro deploy já na fila. 422 se a equipe está acima do limite de aplicações, ou os valores padrão do template falham na validação.


Receitas rápidas

Fazer deploy em todo 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 um deploy terminar:

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 variáveis de ambiente a partir de um arquivo (linhas 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 @-

Ainda não está na API

Console interativo (um TTY pela RPC Exec), conectar um servidor novo, gerenciamento de add-ons além do array addons, criar ou editar regras de notificação / endpoints de webhook / entradas de configuração (a listagem já é só leitura hoje), feed de atividade. Por enquanto, isso só existe no painel; abra uma issue se precisar de algum deles pela API.

MCP (agentes de IA)

POST /api/mcp é um servidor Model Context Protocol (JSON-RPC sobre HTTP com streaming), para que agentes de IA possam usar o Railyard com os mesmos tokens de API.

  • Um token de read consegue listar servidores e aplicações, mostrar uma aplicação e ler seus logs.
  • Um token de write também consegue fazer deploy, reiniciar e definir variáveis de ambiente.
  • Excluir uma aplicação ou remover uma variável de ambiente nunca roda direto: isso cria um pedido de aprovação. Um admin da equipe aprova ou rejeita em Aprovações (também aparece no painel).

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

Ferramentas: list_servers, list_apps, get_app, app_logs, deploy_app, restart_app, set_env, unset_env (aprovação), delete_app (aprovação).