REST API
The team-scoped JSON API behind the CLI: apps, env, deploys, logs and servers.
A small, team-scoped JSON API for driving Railyard from a CLI, CI job, or script — list and create apps, set env, trigger deploys, read deploy logs, check the fleet.
- Base URL — your control plane, under
/api. Local Compose:http://localhost:3000/api - Format — JSON only. Send
Content-Type: application/jsonon writes. No CSRF, no session, no cookies. - Everything is scoped to one team — the team the token belongs to.
Authentication
Every request needs a bearer token:
Authorization: Bearer rly_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Get a token: dashboard → Team → API tokens → name it, pick a scope,
Create. The rly_… string is shown once — copy it then. Admin only.
Scopes
| Scope | Can |
|---|---|
read | every GET |
write | everything read can, plus POST / PATCH / DELETE |
A read-only token on a mutating endpoint → 403 {"error":"this API token is read-only"}.
Missing / bad / revoked / expired token → 401 {"error":"missing or invalid API token"}.
Revoke from the same Team page; a revoked or past-expires_at token stops
working immediately.
Conventions
- Errors — non-2xx responses are
{ "error": "<message>" }. Status codes:400bad request ·401unauthenticated ·403wrong scope ·404not found (or not in your team) ·409conflict (e.g. a deploy is already running) ·422validation failed ·429rate limit. - Timestamps — ISO-8601 UTC (
2026-09-02T14:03:11Z). - IDs — integers, stable per team.
- Pagination — list endpoints that take
limitaccept1–100(default30). No cursor yet; newest-first where ordered by time. - Rate limit — deploy creation is capped per team per hour
(
GET /api/me→limits.deploys_per_hour); over it →429.
Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /api/me | read | Token + team info, limits, counts |
| GET | /api/applications | read | List apps |
| POST | /api/applications | write | Create an app |
| GET | /api/applications/:id | read | App detail |
| PATCH | /api/applications/:id | write | Update an app |
| DELETE | /api/applications/:id | write | Delete an app (and its deploys/logs) |
| POST | /api/applications/:id/archive | write | Stop the container, keep the record |
| POST | /api/applications/:id/unarchive | write | Un-archive |
| GET | /api/applications/:id/env | read | Read env vars |
| PATCH | /api/applications/:id/env | write | Merge env vars (null value deletes) |
| DELETE | /api/applications/:id/env?key=KEY | write | Delete one env var |
| GET | /api/applications/:id/edge | read | Edge settings (same shape as the manifest’s edge: section, see Edge rules) |
| PATCH | /api/applications/:id/edge | write | Merge edge settings: {"edge":{"access_logs":false,"headers":[…],"cors":{…}}} |
| GET | /api/applications/:id/certificates | read | Uploaded certificates (names, covered hosts, validity, fingerprint; never the key) |
| POST | /api/applications/:id/certificates | write | Upload {"certificate":{"certificate_pem":"…","private_key_pem":"…"}} |
| DELETE | /api/applications/:id/certificates/:cert_id | write | Remove one (automatic certificates after the next deploy) |
| GET | /api/applications/:id/domains | read | List custom domains |
| POST | /api/applications/:id/domains | write | Add a custom domain |
| DELETE | /api/applications/:id/domains/:domain_id | write | Remove a custom domain |
| GET | /api/applications/:id/deploys | read | Deploys for one app |
| POST | /api/applications/:id/deploys | write | Trigger a deploy ({"image": "registry/repo:tag"} deploys that prebuilt image once) |
| GET | /api/applications/:id/registry | write | The team’s build registry for deploy --local: repository, username, password, platform |
| POST | /api/applications/:id/tunnel | write | Open a tunnel ({"target": "web|db|redis|<process>", "local_port": 5432}): returns a 12-hour ticket for the TunnelChannel WebSocket, the remote port, and a localhost url for databases; logged as app.tunnel_opened |
| GET | /api/applications/:id/logs | read | Runtime container logs (?tail=, ?process=) |
| GET | /api/applications/:id/processes | read | Per-container state (for ps) |
| POST | /api/applications/:id/restart | write | Bounce every container, or one ?process= |
| GET | /api/applications/:id/process_types | read | Process types with env, shutdown and size settings |
| PATCH | /api/applications/:id/process_types/:pt_id | write | Set env_vars_text, kill_timeout_seconds, stop_signal, size, cpu_limit, memory_limit |
| GET | /api/applications/:id/commands | read | Recent one-off command runs |
| POST | /api/applications/:id/commands | write | Run a one-off command ({"command": "..."}) |
| GET | /api/applications/:id/commands/:cmd_id | read | One run: status, output, exit code |
| POST | /api/applications/:id/reindex | write | Rebuild the search indices: runs the app’s search.reindex_command in a one-off container (202; 422 without a command or a successful deploy, 409 while one runs). Set reindex_command / auto_reindex with PATCH /api/applications/:id; GET shows them under search with the detected library |
| POST | /api/applications/:id/clone | write | Clone the app into a staging copy |
| POST | /api/applications/:id/promote | write | Promote a staging app’s release to production |
| GET | /api/applications/:id/manifest | read | The app as railyard.yml (?secrets=true includes variable values) |
| POST | /api/applications/:id/apply_manifest | write | Apply a manifest ({"manifest": "<yaml text>"}) |
| POST | /api/applications/:id/db_query | read / write | Run SQL ({"sql": "...", "write": false}); write: true needs a write-scoped token |
| GET/POST | /api/applications/:id/database_backups | read / write | List backups / take one now |
| POST | /api/database_backups/:id/restore | write | Restore a backup |
| GET | /api/database_backups/:id/download | read | Signed download link |
| GET/POST | /api/applications/:id/volume_backups | read / write | List backups / back up one volume ({"volume": "name"}) |
| POST | /api/volume_backups/:id/restore | write | Restore a volume backup |
| GET | /api/applications/:id/traffic | read | Requests, p95, error rate and slowest endpoints (?range=1h|24h|7d|30d) |
| PATCH | /api/applications/:id/autoscale | write | Scale web replicas on p95/CPU ({"max":N,"min":N,"p95":ms,"cpu":pct}); {"max":null} turns it off |
| POST | /api/applications/:id/data_move | write | Move the app’s data to a database server ({"database_server_id": 12}); 202 with what gets copied |
| POST | /api/applications/:id/external_datastores | write | Point the app’s database, redis or elasticsearch at an outside URL ({"kind","url","copy"}); the URL is checked first (502 if unreachable), 202 when queued |
| DELETE | /api/applications/:id/external_datastores/:kind | write | Bring that data back under Railyard (?copy=true copies it back) |
| GET | /api/applications/:id/addons/:kind/transfers | read | Recent exports/imports of an add-on (redis, mongodb, elasticsearch, storage, influxdb, rabbitmq), with download links |
| POST | /api/applications/:id/addons/:kind/transfers | write | Export the add-on’s data; with multipart file + confirm=<app name>, load a file into it |
| GET | /api/applications/:id/storage/objects | read | Browse the storage add-on’s bucket (?prefix=, ?token=): folders, files with size, public flag and a signed link |
| POST | /api/applications/:id/storage/objects | write | Upload a file (multipart file, prefix or key) |
| DELETE | /api/applications/:id/storage/object?key= | write | Delete a file, or a folder when the key ends in / |
| PATCH | /api/applications/:id/storage/visibility | write | Make a folder public or private ({"prefix":"avatars/","public":true}) |
| GET | /api/applications/:id/redis | read | Where the app’s Redis lives (shared db N or its own), its limit and policy, memory used |
| PATCH | /api/applications/:id/redis | write | {"dedicated":true,"maxmemory_mb":512,"policy":"allkeys-lru"} gives the app its own Redis (keys copied) or changes its limit; {"dedicated":false} moves it back |
| GET | /api/deploys | read | Deploys across the team (?application_id=, ?limit=) |
| GET | /api/deploys/:id | read | Deploy detail |
| GET | /api/deploys/:id/logs | read | Deploy log lines (oldest first, ≤5000) |
| GET | /api/applications/:id/log_search | read | Stored log lines: time range, search, cursor pages (see Logs, metrics and alerts) |
| GET | /api/applications/:id/log_download | read | Stored log lines for a range as text |
| GET/PATCH/DELETE | /api/applications/:id/otel_export | read / write | OpenTelemetry export settings (see Logs, metrics and alerts) |
| GET/POST | /api/applications/:id/metric_alerts | read / write | Metric threshold alerts and their history |
| PATCH/DELETE | /api/applications/:id/metric_alerts/:alert_id | write | Change or remove an alert |
| GET | /api/applications/:id/job_queue | read | Job queue stats, failed jobs, history (see Logs, metrics and alerts) |
| POST | /api/applications/:id/job_queue/failed/:job_id/retry | write | Retry a failed job |
| DELETE | /api/applications/:id/job_queue/failed/:job_id | write | Discard a failed job |
| GET | /api/servers | read | The fleet |
| GET | /api/servers/:id | read | Server detail + live process states |
| POST | /api/servers/:id/reboot | /power_cycle | /cordon | /uncordon | /prune_docker | write | Server lifecycle actions |
| PATCH | /api/servers/:id/resize | write | Resize at the provider ({"size": "..."}) |
| GET | /api/notification_rules | read | Team-wide notification rules |
| GET | /api/webhook_endpoints | read | Team-wide webhook endpoints |
| GET | /api/config | read | Team-wide config entries |
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
app object (list form):
{
"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 adds: 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[] (names only — values are
never returned in app detail; use the env endpoint),
domains[] {hostname, redirect, cert_state}.
Create — 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"}
}}'
Body is {"application": { … }}. Accepted keys: 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
(one name: command per line), scheduled_tasks_text
(name | cron | command per line), db_extensions,
deploy_approval_required, strict_pushes (a git push whose build
fails is rejected; default true), addons (array), env_vars (object).
On update also: server_id.
server(orserver_id) may be a server id or hostname. Omit it and the app lands on the first uncordoned server in the team.- Blank
hostnameis derived —<name>.<server app_domain>, elseAPP_DOMAIN_SUFFIX, else<name>.<server-ip>.sslip.io. - Creating does not deploy. Follow with
POST …/deploys. - Over the team app limit →
422.
201 with the detailed app object. PATCH → 200. DELETE → 204.
Archive / unarchive — POST /api/applications/:id/archive stops the
running container and sets archived: true; the record, config and history
stay. unarchive clears the flag (redeploy to bring it back).
Env vars — /api/applications/:id/env
# read
curl -s https://cp.example.com/api/applications/11/env -H "Authorization: Bearer $RLY"
# {"env":{"RAILS_ENV":"production","LOG_LEVEL":"info"}}
# merge (set two, delete one)
curl -s -X PATCH https://cp.example.com/api/applications/11/env \
-H "Authorization: Bearer $RLY" -H "Content-Type: application/json" \
-d '{"env":{"LOG_LEVEL":"debug","FEATURE_X":"1","OLD_KEY":null}}'
# delete one
curl -s -X DELETE "https://cp.example.com/api/applications/11/env?key=FEATURE_X" \
-H "Authorization: Bearer $RLY"
PATCH merges into the existing set; a null value removes that key.
All three return the full {"env": { … }}. Changes take effect on the next
deploy.
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 makes the hostname 301 to the app’s primary hostname
instead of serving it. Point the shown dns_target at your DNS and Caddy
issues the cert. DELETE …/domains/:domain_id → 204.
Deploys
deploy object:
{
"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 adds error, triggered_by (email),
duration_seconds.
A canary deploy also has a canary object (null otherwise):
"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 starts the promotion
deploy (202, promotion_deploy_id); DELETE /api/deploys/:id/canary
rolls it back. Both need a write token and answer 409 once the canary is
no longer live.
Deploy strategy — PATCH /api/applications/:id/deploy_strategy with any
of deploy_strategy (standard · canary), canary_percent (1–99),
canary_steps_text (comma-separated ascending percentages, e.g. "5, 25, 50";
blank = one step), canary_promote_after_minutes (1–1440, empty = by hand),
canary_max_error_rate (0–100). GET /api/applications/:id shows the same
fields. With steps set, promoting a step starts the next step as its own
canary instead of going straight to 100%.
Redeploy when the image changes (image apps only, else 422) —
PATCH /api/applications/:id/image_watch with enabled (true/false);
POST /api/applications/:id/image_watch/check checks the registry now
(202). Both return, and GET /api/applications/:id shows as
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 }
Registries call POST /webhooks/image/<token> (no API token; any body) to
trigger a check.
Trigger — 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(optional) — branch, tag, or SHA to deploy once; defaults to the app’sgit_ref, which arefnever changes. The deploy’striggerisrefand itsreffield shows what it used. Names with characters git refs can’t have get422. Ignored for animage://app.message(optional) — a deploy note (up to 500 characters), returned asmessageand shown in the dashboard, activity, notifications and thedeploy.*webhooks.at(optional) — ISO 8601 time to deploy at instead of now. Answers201withstatus: "scheduled"andscheduled_for; the time must be in the future and inside the deploy window (422otherwise). Cancel it withPOST /api/deploys/:id/cancel(also cancels a queued or running deploy).ignore_window(optional) — deploy even though the app is outside its deploy window. Without it such a deploy answers409withoutside_window: trueandwindow_opens_at.rollback_to(optional) — id of an earlier succeeded deploy. Redeploys its commit and puts back the variables it ran with; the response addsrestored_config. Send"restore_config": falseto roll back the code only. Releases deployed before Railyard kept a copy of their variables always roll back code only.202 Accepted+ the deploy object when it’s queued and running.200 OK+ the object withstatus: "pending_approval"if the app requires deploy approval (approve it in the dashboard).409if a deploy is already active or awaiting approval for that app.429if the team’s deploys-per-hour limit is hit.422if the app is archived.422withmissing_env(array of names) if the app has never deployed successfully and credentials its repo documents are unset or still placeholders. Set them via the env endpoint, or send"force": trueto deploy anyway — those names aren’t asked for again.
Deploy window — 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"}'
Returns {days, start, end, time_zone, open_now, opens_at} (also under
deploy_window in GET /api/applications/:id). "days": [] removes the
window. Pushes, GitHub/GitLab webhooks and coalesced pushes outside the
window become one scheduled deploy that starts when it opens.
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. Capped at 5000 lines, oldest
first. Poll while status is queued/running.
List — GET /api/deploys?application_id=11&limit=50 or
GET /api/applications/11/deploys.
Runtime — logs, processes, commands, restart
The programmatic half of the app-page console. Everything here goes to the agent on the box over an mTLS channel.
Runtime logs — GET /api/applications/:id/logs?tail=200&process=web
What the running app is printing now — not the build output (that’s
/api/deploys/:id/logs). tail is 1–5000 (default 200); process filters to
one process. 409 with {"error": "No running deployment"} when nothing is
up yet, or the agent is unreachable.
{
"application_id": 11,
"lines": [
{ "stream": "stdout", "process": "web", "line": "GET / 200 12ms", "at": "2026-09-06T12:00:01Z" }
]
}
There is no server-push follow yet — railyard logs --follow re-requests on
an interval.
Processes — GET /api/applications/:id/processes
Last-reported state of every container, refreshed each agent heartbeat. Empty list = nothing running.
{ "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]
Bounces every container, or just one process’s. 409 if nothing is running.
{ "restarted": 2, "processes": ["railyard-app-11-web", "railyard-app-11-worker-1"] }
Process types — 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 is small, medium, large (mapped from PROCESS_SIZE_*), custom
(then send cpu_limit and memory_limit) or blank for the app-wide limit.
cpus / memory are the effective limits after that fallback.
kill_timeout_seconds (1–300, blank for the default) is how long the process
gets after stop_signal (SIGTERM, SIGINT, SIGQUIT, blank for the
image’s own) before it is killed. Changes apply on the next deploy. An invalid
value → 422.
One-off commands — 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"}'
Runs inside the web container via the agent’s RunTask RPC. 202 Accepted
with the row; poll GET /api/applications/:id/commands/:cmd_id for status
(queued → running → succeeded/failed), streamed output, and
exit_code. GET /api/applications/:id/commands lists recent runs.
Interactive console (a TTY, Exec RPC) is not in the API yet.
Servers (read-only)
curl -s https://cp.example.com/api/servers -H "Authorization: Bearer $RLY"
server object:
{
"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 adds apps[] (names) and
processes[] {container, state, restarts, crash_looping}.
Cordoning, rebooting, power-cycling, pruning and resizing an existing server
are POST/PATCH on /api/servers/:id (see the table above). Attaching a
new server is dashboard-only for now.
Data maintenance window — GET/PATCH /api/maintenance_window (team) and
GET/PATCH /api/servers/:id/maintenance_window (one server; blank day = use the
team’s). Fields: maintenance_day 0–6 (Sunday = 0), maintenance_hour 0–23 UTC,
maintenance_hours 1–12. See Database servers.
Postgres major upgrades — GET/POST /api/servers/:id/postgres_upgrades
(to_image; creating one runs the compatibility check), GET …/:id (with the
log), PATCH …/:id (when=window to wait for the maintenance window, otherwise
it starts now), POST …/:id/rollback, DELETE …/:id (removes the kept old
version, or drops an unstarted plan).
Connection pooler — GET/POST/DELETE /api/servers/:id/connection_pooler
(PgBouncer, transaction mode, port 6432). Per app: PATCH /api/applications/:id/database with app_database[pooled]=true points
DATABASE_URL at the pooler from the next deploy and keeps
DATABASE_DIRECT_URL for migrations.
Database forks — GET/POST /api/applications/:id/database_forks
(target_server_id for a new database or target_application_id for a
staging/preview env, source=live|backup, optional mask_sql),
GET …/:id (with the fork’s URL once ready), DELETE …/:id (drops it).
Standby replicas — GET/POST /api/servers/:id/database_replicas
(replica_server_id), POST …/:id/promote, DELETE …/:id.
Quick recipes
Deploy on every 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\"}"
Wait for a deploy to finish:
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
Sync env from a file (KEY=value lines):
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
Interactive console (a TTY over the Exec RPC), attaching a new server,
addon management beyond the addons array, creating or editing notification
rules / webhook endpoints / config entries (listing is read-only today),
activity feed. These are dashboard-only today; open an issue if you need one
over the API.
MCP (AI agents)
POST /api/mcp is a Model Context Protocol server (JSON-RPC over streamable HTTP), so AI agents can use Railyard with the same API tokens.
- A read token can list servers and apps, show an app and read its logs.
- A write token can also deploy, restart and set env vars.
- Deleting an app or removing an env var never runs directly: it creates an approval request. A team admin approves or rejects it under Approvals (it also shows on the dashboard).
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_..." } } } }
Tools: list_servers, list_apps, get_app, app_logs, deploy_app, restart_app, set_env, unset_env (approval), delete_app (approval).