VRAMfarm API
Launch GPU servers from scripts and CI: find an available machine, start it with a ready-made app or your own Docker image, wait for SSH, stop it. Plain HTTPS and JSON, no SDK needed.
https://vramfarm.com/api- Responses are JSON, UTF-8. POST requests need the
Content-Type: application/jsonheader. - Money is in euros.
priceis per hour for the whole server, all cards included, as a decimal with cents (for example0.39). - Times in responses are ISO 8601 in UTC; the
updatedfield in the catalogue is Unix milliseconds. - Error messages come in your account language, or in the language of the
Accept-Languageheader, falling back to English.
Quick start
The script launches an RTX 4090 server, waits until it is ready, prints the SSH command and stops the server, even if something fails halfway.
import os, time, requests
API = "https://vramfarm.com/api"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['GF_TOKEN']}"
r = s.post(f"{API}/instances", json={"gpu": "rtx-4090", "count": 1, "template": "pytorch"})
r.raise_for_status()
sid = r.json()["id"]
try:
while True:
srv = s.get(f"{API}/instances/{sid}").json()
if srv["status"] == "running":
break
if srv["status"] == "stopped":
raise RuntimeError(srv["error"])
print(srv["stage_note"] or srv["stage"])
time.sleep(10)
print(srv["access"]["ssh"]["command"])
# your work goes here: ssh, scp, requests to the app on the server
finally:
s.post(f"{API}/instances/stop", json={"id": sid})// Node.js 18+, .mjs file
const API = "https://vramfarm.com/api";
const headers = { Authorization: `Bearer ${process.env.GF_TOKEN}`, "Content-Type": "application/json" };
const call = async (path, body) => {
const r = await fetch(API + path, body ? { method: "POST", headers, body: JSON.stringify(body) } : { headers });
const data = await r.json();
if (!r.ok) throw new Error(`${r.status} ${data.code}: ${data.error}`);
return data;
};
const { id } = await call("/instances", { gpu: "rtx-4090", count: 1, template: "pytorch" });
try {
let srv;
while ((srv = await call(`/instances/${id}`)).status !== "running") {
if (srv.status === "stopped") throw new Error(srv.error);
console.log(srv.stage_note ?? srv.stage);
await new Promise((ok) => setTimeout(ok, 10_000));
}
console.log(srv.access.ssh.command);
} finally {
await call("/instances/stop", { id });
}#!/usr/bin/env bash
# needs curl and jq
set -euo pipefail
API=https://vramfarm.com/api; H="Authorization: Bearer $GF_TOKEN"; J="Content-Type: application/json"
ID=$(curl -sf "$API/instances" -H "$H" -H "$J" \
-d '{"gpu":"rtx-4090","count":1,"template":"pytorch"}' | jq -r .id)
trap 'curl -sf "$API/instances/stop" -H "$H" -H "$J" -d "{\"id\":$ID}" >/dev/null' EXIT
for i in $(seq 1 120); do
S=$(curl -sf "$API/instances/$ID" -H "$H")
ST=$(jq -r .status <<<"$S")
[ "$ST" = running ] && break
[ "$ST" = stopped ] && { echo "failed to start: $(jq -r .error <<<"$S")"; exit 1; }
echo " $(jq -r '.stage_note // .stage' <<<"$S")"; sleep 10
done
SSH=$(jq -r .access.ssh.command <<<"$S")
$SSH -o StrictHostKeyChecking=accept-new 'cd /workspace && python train.py'Authentication
Create a token in the dashboard → Settings → API tokens. A token looks like gf_live_… and is shown once: store it in your CI secrets or in the GF_TOKEN environment variable. You can revoke tokens in the same place. Up to 10 tokens can be active.
Send the token in the Authorization: Bearer gf_live_… header. Nothing else is needed: requests are not signed, there is no timestamp or recvWindow, and cookies and Origin are not checked for token requests.
import os, requests
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
print(requests.get("https://vramfarm.com/api/account", headers=H).json())const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
console.log(await fetch("https://vramfarm.com/api/account", { headers: H }).then((r) => r.json()));export GF_TOKEN=gf_live_xxxxxxxxxxxxxxxxxxxxxxxx
curl -s https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"A token only opens the methods on this page. Your password, SSH key and issuing new tokens are available only in the dashboard; with a token those endpoints return 403. A leaked token lets someone launch and stop servers, but not manage your account.
The catalogue and /health work without a token. But if you send an Authorization header with a wrong or revoked token, every endpoint returns 401.
Call the API from your own server, script or CI. Requests from a browser on another website will not go through, and any visitor could see the token there.
Before your first launch, add a public SSH key in the dashboard settings: it is installed on every new server. You can check that everything is ready with GET /api/account.
Errors and limits
On an error you get an HTTP status and the body {"error": "message", "code": "snake_case_code"}. The error text is safe to show to a person as is; branch your code on code, never on the text. Some responses add a hint for programs: unverified on launch, saving on stop.
| Status | When |
|---|---|
400 | Invalid parameters: image, disk, not enough VRAM, server limit |
401 | Wrong or revoked token, or the account is blocked |
402 | Not enough balance; error says how much is needed |
403 | Email not confirmed, or the method is not available with a token |
404 | Server not found or it belongs to another account; unknown endpoint |
409 | No machines of this type are available; the server folder is still being saved |
415 | POST without Content-Type: application/json |
429 | Rate limit exceeded; wait as many seconds as the Retry-After header says |
500 | An error on our side |
502 | The catalogue is temporarily unavailable |
503 | The machine provider is overloaded; retry the launch in a minute |
Limits
- 60 requests per minute per token, across all methods. One token’s limit does not affect others.
- 10 launches per minute per token. Failed launches count too.
- The catalogue is additionally limited to 60 requests per minute per IP address.
- Up to 4 servers at a time; the exact number is in
max_active_instances. Need more? Write to us. - Request body up to 64 KB.
Server status changes at most every few seconds, so poll it every 5–10 seconds.
import time, requests
def call(method, url, **kw):
for attempt in range(5):
r = requests.request(method, url, **kw)
if r.status_code not in (429, 502, 503):
return r
time.sleep(int(r.headers.get("Retry-After", 30)))
return rasync function call(url, opts = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(url, opts);
if (![429, 502, 503].includes(r.status)) return r;
await new Promise((ok) => setTimeout(ok, (+r.headers.get("Retry-After") || 30) * 1000));
}
throw new Error("API is not responding");
}curl -s --retry 5 --retry-delay 30 --retry-all-errors \
https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"Catalogue
Available machines
GET/api/offers
A list of available machines with their price in euros per hour, cheapest first. The catalogue refreshes every 10 minutes.
Query parameters
| Parameter | Type | Description |
|---|---|---|
gpuoptional | string | GPU model, for example h200. Without it, or with an unknown model, all models are returned. The model list is below the table. |
countoptional | integer | Number of cards in the machine: 1, 2, 4 or 8. Other values are ignored. |
alloptional | string | 1 also shows unverified machines. By default only verified ones (verified: true). |
Models: b200 h200 h100 a100 a100-40 l40s l40 l4 rtx-pro-6000 rtx-a6000 rtx-a5000 rtx-a4000 rtx-5090 rtx-5080 rtx-5070-ti rtx-4090 rtx-4080 rtx-4060-ti rtx-3090 rtx-3080 rtx-3060. a100 is 80 GB, a100-40 is 40 GB.
Machine fields
id | integer | Pass it as offer_id when launching |
gpu, gpu_name | string | Model: code for requests and display name |
num_gpus | integer | Number of cards |
vram | integer | VRAM of one card, GB |
cpu, cpu_cores, cpu_ghz | string, integer, number | CPU, available cores, clock speed |
ram | integer | RAM, GB |
disk, disk_bw | integer | How many GB of disk you can take; disk speed, MB/s |
net_down, net_up | integer | Network, Mbit/s |
country | string | Country, ISO code |
reliability | number | Machine reliability from 0 to 1 |
cuda, driver | number, string | Highest supported CUDA and driver version |
dlperf, tflops, mem_bw, pcie | number | Performance: DLPerf score, TFLOPS, memory and PCIe bandwidth |
verified | boolean | The machine is verified |
price | number | EUR per hour for the whole server, with the template’s minimum disk included |
disk_eur | number | EUR per hour for 1 GB of disk above the included amount |
traffic_eur, traffic_up_eur | number | EUR per 1 GB of downloaded and uploaded traffic |
duration | integer | How many more days the machine can be rented |
static_ip, ports | boolean, integer | Static IP, number of open ports |
If live is false, the catalogue is switched off and offers is empty. updated is the response time in Unix milliseconds.
Errors
401Wrong or revoked token429More than 60 requests per minute502The catalogue is temporarily unavailable; retry in a minute
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
r = requests.get(f"{API}/offers", headers=H, params={"gpu": "h200", "count": 4})
r.raise_for_status()
for o in r.json()["offers"][:5]:
print(o["id"], o["gpu_name"], o["price"], "EUR/h", o["country"])const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
const r = await fetch(`${API}/offers?gpu=h200&count=4`, { headers: H });
const { offers } = await r.json();
console.log(offers.slice(0, 5).map((o) => [o.id, o.gpu_name, o.price, o.country]));curl -s "https://vramfarm.com/api/offers?gpu=h200&count=4" \
-H "Authorization: Bearer $GF_TOKEN"{
"live": true,
"updated": 1761040000000,
"offers": [
{
"id": 18273645,
"gpu": "h200",
"gpu_name": "H200",
"num_gpus": 4,
"vram": 141,
"cpu": "AMD EPYC 9354",
"cpu_cores": 64,
"ram": 512,
"disk": 2000,
"net_down": 8000,
"net_up": 8000,
"country": "DE",
"reliability": 0.998,
"cuda": 12.8,
"verified": true,
"price": 33.16,
"disk_eur": 0.00005,
"traffic_eur": 0.007,
"traffic_up_eur": 0.007
}
]
}Machine types
GET/api/offer-types
One row per “model × number of cards” pair: the cheapest machine with reliability of 0.97 or more and how many such machines are available. Handy for picking gpu and count for a launch.
No parameters.
Fields are the same as for a catalogue machine, plus available, how many machines of this type are free, and max_dlperf, the best performance score among them. The response is cached for a minute.
Errors
401Wrong or revoked token429More than 60 requests per minute502The catalogue is temporarily unavailable
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
types = requests.get(f"{API}/offer-types", headers=H).json()["types"]
for t in types:
print(f'{t["num_gpus"]}x {t["gpu_name"]}: from {t["price"]} EUR/h, {t["available"]} available')const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
const { types } = await fetch(`${API}/offer-types`, { headers: H }).then((r) => r.json());
for (const t of types) console.log(`${t.num_gpus}x ${t.gpu_name}: from ${t.price} EUR/h`);curl -s https://vramfarm.com/api/offer-types -H "Authorization: Bearer $GF_TOKEN"{
"live": true,
"updated": 1761040000000,
"types": [
{
"id": 20311822,
"gpu": "rtx-3090",
"gpu_name": "RTX 3090",
"num_gpus": 1,
"vram": 24,
"price": 0.3,
"reliability": 0.991,
"available": 37,
"max_dlperf": 41
}
]
}Servers
Launch a server
POST/api/instances
Rents a machine and starts the chosen app or your Docker image on it. The response comes back immediately; the server takes 2–5 minutes to get ready, or 8–15 with your own image.
Request body, JSON
| Parameter | Type | Description |
|---|---|---|
offer_idoptional | integer | Machine id from the catalogue. If it has been taken, we pick a similar one of the same type. Either offer_id or gpu is required. |
gpuoptional | string | Model, if the exact machine doesn’t matter: we take the cheapest reliable one that fits. |
countoptional | integer | Number of cards when choosing by gpu, 1 to 8. Default 1. |
templateoptional | string | What to run, pytorch by default. See the table below. An unknown value is treated as pytorch. |
imageoptional | string | Required for custom: a public image, written as in docker pull. |
cmdoptional | string | For custom: a command to run at start, up to 4000 characters. |
diskoptional | integer | Disk, GB. Less than the template minimum is raised to the minimum. No more than 2000 and no more than the machine has. |
ws_restoreoptional | boolean | Default true: your saved working folder comes back on the server. false gives a clean server. |
Templates (template)
| Code | What’s inside | Disk from | Card memory from |
|---|---|---|---|
pytorch | PyTorch, CUDA, Jupyter | 60 GB | |
jupyter | Jupyter | 40 GB | |
custom | Your image from image | 60 GB | |
comfyui | ComfyUI and Stable Diffusion 1.5 | 100 GB | |
comfy-flux | ComfyUI and Flux schnell | 100 GB | 16 GB |
comfy-sdxl | ComfyUI and SDXL | 100 GB | 12 GB |
comfy-wan | ComfyUI and Wan 2.2 5B, video | 100 GB | 24 GB |
comfy-wan14 | ComfyUI and Wan 2.2 14B, video | 160 GB | 48 GB |
comfy-qwen | ComfyUI and Qwen-Image | 100 GB | 24 GB |
comfy-qedit | ComfyUI and Qwen-Image-Edit | 100 GB | 24 GB |
comfy-hidream | ComfyUI and HiDream-O1 | 60 GB | 24 GB |
chat | Open WebUI and Ollama, model qwen3:8b | 80 GB | 12 GB |
chat-pro | Open WebUI and Ollama, model qwen3:32b | 100 GB | 24 GB |
chat-mimo | Open WebUI and Ollama, model MiMo 9B | 60 GB | 12 GB |
vllm | vLLM, model Qwen/Qwen2.5-7B-Instruct | 100 GB | |
tts-qwen3 | Qwen3-TTS text to speech, Gradio | 40 GB | 12 GB |
tts-voxcpm | VoxCPM2 text to speech, Gradio | 40 GB | 12 GB |
tts-chatterbox | Chatterbox text to speech, Gradio | 40 GB | 12 GB |
Billing runs by the minute from the moment the server becomes running, with a minimum of 15 minutes per launch. Your balance must cover at least one hour of all your servers including the new one. When the balance runs out, servers stop and their disks are wiped.
An identical request within 15 seconds returns the server already created, with "duplicate": true; we won’t rent a second machine. If a launch fails because of the balance or an unconfirmed email, it is not queued for token requests: retry it yourself.
Errors
400No GPU given, invalid image, disk too large, not enough VRAM for the template, or already 4 servers402Not enough balance for an hour; the amount is inerror403Email not confirmed; the response has"unverified": true409No machines of this type are available429More than 10 launches per minute per token503The machine provider is overloaded; retry in a minute
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
r = requests.post(f"{API}/instances", headers=H, json={
"gpu": "h200", "count": 4, "template": "custom",
"image": "nvidia/cuda:12.4.1-devel-ubuntu22.04", "disk": 200,
"cmd": "pip install -q torch && python /workspace/train.py",
})
if not r.ok:
raise SystemExit(f"{r.status_code} {r.json()['code']}: {r.json()['error']}")
print("server", r.json()["id"])const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}`, "Content-Type": "application/json" };
const r = await fetch(`${API}/instances`, {
method: "POST", headers: H,
body: JSON.stringify({
gpu: "h200", count: 4, template: "custom",
image: "nvidia/cuda:12.4.1-devel-ubuntu22.04", disk: 200,
cmd: "pip install -q torch && python /workspace/train.py",
}),
});
const data = await r.json();
if (!r.ok) throw new Error(`${r.status} ${data.code}: ${data.error}`);
console.log("server", data.id);curl -s https://vramfarm.com/api/instances \
-H "Authorization: Bearer $GF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"gpu":"h200","count":4,"template":"custom",
"image":"nvidia/cuda:12.4.1-devel-ubuntu22.04","disk":200,
"cmd":"pip install -q torch && python /workspace/train.py"}'{
"ok": true,
"id": 1234,
"status": "provisioning"
}List servers
GET/api/instances
Your servers, newest first, up to 100. By default: running, starting, and stopped in the last 7 days.
Query parameters
| Parameter | Type | Description |
|---|---|---|
statusoptional | string | provisioning, running or stopped: only servers in that state. |
Each item of instances has the same shape as the GET /api/instances/{id} response.
Errors
401Wrong or revoked token
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
r = requests.get(f"{API}/instances", headers=H, params={"status": "running"})
for s in r.json()["instances"]:
print(s["id"], s["gpu_name"], s["cost_eur"], "EUR")const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
const { instances } = await fetch(`${API}/instances?status=running`, { headers: H }).then((r) => r.json());
for (const s of instances) console.log(s.id, s.gpu_name, s.cost_eur);curl -s "https://vramfarm.com/api/instances?status=running" \
-H "Authorization: Bearer $GF_TOKEN"{
"instances": [
{ "id": 1234, "status": "running", "gpu": "h200", "gpu_count": 4, "cost_eur": 0.55, "...": "..." }
]
}Server and access
GET/api/instances/{id}
The state of one server. Once status is running, access holds SSH, Jupyter and the app address.
Path parameters
| Parameter | Type | Description |
|---|---|---|
idrequired | integer | id from the launch response |
Response fields
status | string | provisioning: getting ready; running: running and billed; stopped: stopped |
stage, stage_note | string | null | Only while provisioning: the stage (for example pick, loading) and what is happening, in your account language |
gpu, gpu_name, gpu_count | string, integer | Model and number of cards |
template, image | string | Template and image (for custom) |
disk_gb | integer | Disk, GB |
price_eur_hour | number | Price per hour for the whole server, EUR |
cost_eur | number | Charged for this launch, EUR; for a running server, up to the current minute |
traffic_eur | number | Charged for traffic, EUR |
specs | object | null | Machine specs |
access.ssh | object | null | host, port, user and a ready command |
access.jupyter | string | null | Jupyter link with a token |
access.app | string | null | App address for ComfyUI and vLLM; for custom, if the image opens its own port. More in Apps on the server |
access.workdir | string | Working folder, /workspace |
error | string | null | Why the server stopped on its own: the image failed to download, the balance ran out |
created_at, started_at, stopped_at | string | null | Time in ISO 8601, UTC |
access is filled only for a running server. The SSH key is the one added in your dashboard settings. If a machine fails to start, we take another one for free; the server id stays the same.
ssh -p 31001 root@vramfarm.com nvidia-smi
scp -P 31001 root@vramfarm.com:/workspace/out.ckpt ./Errors
404No server with this id, or it isn’t yours
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
s = requests.get(f"{API}/instances/1234", headers=H).json()
if s["status"] == "running":
print(s["access"]["ssh"]["command"])
else:
print(s["status"], s["stage_note"] or s["error"])const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
const s = await fetch(`${API}/instances/1234`, { headers: H }).then((r) => r.json());
console.log(s.status === "running" ? s.access.ssh.command : s.stage_note ?? s.error);curl -s https://vramfarm.com/api/instances/1234 -H "Authorization: Bearer $GF_TOKEN"{
"id": 1234,
"status": "running",
"stage": null,
"stage_note": null,
"gpu": "h200",
"gpu_name": "H200",
"gpu_count": 4,
"template": "custom",
"image": "nvidia/cuda:12.4.1-devel-ubuntu22.04",
"disk_gb": 200,
"price_eur_hour": 33.16,
"cost_eur": 0.55,
"traffic_eur": 0,
"specs": {
"vram_gb": 141, "cpu": "AMD EPYC 9354", "cpu_cores": 64, "ram_gb": 512,
"country": "DE", "cuda": 12.8, "net_down_mbps": 8000, "net_up_mbps": 8000
},
"access": {
"ssh": {
"host": "vramfarm.com",
"port": 31001,
"user": "root",
"command": "ssh -p 31001 root@vramfarm.com"
},
"jupyter": "https://u.vramfarm.com/api/j/…/lab?token=…",
"app": null,
"workdir": "/workspace"
},
"error": null,
"created_at": "2026-10-21T10:00:00.000Z",
"started_at": "2026-10-21T10:04:12.000Z",
"stopped_at": null
}Stop a server
POST/api/instances/stop
Stops the server; billing stops immediately. The server disk is wiped; you can save the working folder.
Request body, JSON
| Parameter | Type | Description |
|---|---|---|
idrequired | integer | Server id |
saveoptional | boolean | true: save /workspace first, then stop. Does not work for custom. |
forceoptional | boolean | true: stop right away, even if the folder is still being saved. Saving is interrupted. |
Stopping a server that is already stopped returns {"ok":true,"already":true}. It is not an error, so you can put the call in finally or a trap on script exit.
With "save": true the response {"ok":true,"saving":true} comes back immediately, and the server saves the folder and stops on its own, usually within a few minutes. Storage costs €0.03 per GB per month, charged daily. Your next server gets the folder automatically unless you pass "ws_restore": false.
Errors
404No server with this id, or it isn’t yours409The folder is still being saved; the response has"saving": true. Wait, or pass"force": true
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
r = requests.post(f"{API}/instances/stop", headers=H, json={"id": 1234})
print(r.json())const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}`, "Content-Type": "application/json" };
const r = await fetch(`${API}/instances/stop`, { method: "POST", headers: H, body: JSON.stringify({ id: 1234 }) });
console.log(await r.json());curl -s https://vramfarm.com/api/instances/stop \
-H "Authorization: Bearer $GF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":1234}'{
"ok": true
}Account
Account
GET/api/account
Balance and readiness to launch. Handy to check before launching that your email is confirmed and an SSH key is added.
No parameters.
email | string | Account email |
verified | boolean | Email confirmed. Without it, a launch returns 403 |
balance_eur | number | Balance, EUR |
ssh_key_set | boolean | SSH key added. Without a key the server still starts, but you can only get in through Jupyter |
active_instances | integer | How many servers are starting or running now |
max_active_instances | integer | How many you can run at the same time |
Errors
401Wrong or revoked token, or the account is blocked
import os, requests
API = "https://vramfarm.com/api"
H = {"Authorization": f"Bearer {os.environ['GF_TOKEN']}"}
acc = requests.get(f"{API}/account", headers=H).json()
assert acc["verified"] and acc["ssh_key_set"], acc
print(acc["balance_eur"], "EUR")const API = "https://vramfarm.com/api";
const H = { Authorization: `Bearer ${process.env.GF_TOKEN}` };
const acc = await fetch(`${API}/account`, { headers: H }).then((r) => r.json());
console.log(acc.balance_eur, acc.verified, acc.ssh_key_set);curl -s https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"{
"email": "you@example.com",
"verified": true,
"balance_eur": 150.0,
"ssh_key_set": true,
"active_instances": 0,
"max_active_instances": 4
}Service
Service health
GET/api/health
Checks that the API is up. No token needed.
No parameters.
On a failure the response is 503 with a short reason in why, for example {"ok":false,"why":"…"}.
Errors
503The service is not fully working; the reason is inwhy
import requests
r = requests.get("https://vramfarm.com/api/health", timeout=10)
print(r.status_code, r.json())const r = await fetch("https://vramfarm.com/api/health");
console.log(r.status, await r.json());curl -s https://vramfarm.com/api/health{
"ok": true,
"status": "ok"
}Apps on the server via API
ComfyUI, the text-to-speech apps, the chat and vLLM each have their own API. Use it to generate images, audio and text from a script without opening the interface. Your VRAMfarm token is not needed by these apps and is never sent to them.
Where to find the app address:
- ComfyUI and vLLM: the
access.appfield in GET /api/instances/{id}, likehttps://u.vramfarm.com/api/a/<key>/. For vLLM it already ends inv1. - Chat and text to speech: an address like
http://<host>:<port>is shown on the server card in the dashboard; it is not inaccess.app. From a script, an SSH tunnel is more reliable: it works whenever SSH does. - Your own image (
custom):access.appis filled if the container opens a port other than 22, 8080 and 8888.
# SSH port from access.ssh.port; -N: tunnel only, no shell
ssh -p 31001 root@vramfarm.com -N -L 11434:127.0.0.1:11434Keep the tunnel open while your script runs: requests to localhost:11434 go to the server. For text to speech use port 7860 (-L 7860:127.0.0.1:7860), for ComfyUI 8188, for vLLM 8000. The app starts 5–10 minutes after running; until then the address returns 502 or does not respond.
ComfyUI
Templates comfyui and comfy-*. Build a workflow in the interface and save it with Workflow → Export (API) as workflow_api.json. Then the script changes the prompt text in the workflow, sends it to /prompt, waits for the result in /history/{prompt_id} and downloads the images through /view.
import json, time, requests
APP = "https://u.vramfarm.com/api/a/<key>/" # access.app of the server
wf = json.load(open("workflow_api.json")) # Workflow → Export (API) in ComfyUI
wf["6"]["inputs"]["text"] = "a cat in a spacesuit, photo" # node number from your file
pid = requests.post(APP + "prompt", json={"prompt": wf}).json()["prompt_id"]
while pid not in (h := requests.get(APP + f"history/{pid}").json()):
time.sleep(2)
for node in h[pid]["outputs"].values():
for img in node.get("images", []):
data = requests.get(APP + "view", params=img).content # filename, subfolder, type
open(img["filename"], "wb").write(data)import { readFile, writeFile } from "node:fs/promises";
const APP = "https://u.vramfarm.com/api/a/<key>/"; // access.app of the server
const wf = JSON.parse(await readFile("workflow_api.json", "utf8"));
wf["6"].inputs.text = "a cat in a spacesuit, photo";
const { prompt_id } = await fetch(APP + "prompt", {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: wf }),
}).then((r) => r.json());
let h = {};
while (!h[prompt_id]) {
await new Promise((ok) => setTimeout(ok, 2000));
h = await fetch(APP + `history/${prompt_id}`).then((r) => r.json());
}
for (const node of Object.values(h[prompt_id].outputs))
for (const img of node.images ?? []) {
const r = await fetch(APP + "view?" + new URLSearchParams(img));
await writeFile(img.filename, Buffer.from(await r.arrayBuffer()));
}APP="https://u.vramfarm.com/api/a/<key>/"
curl -s "$APP"prompt -H "Content-Type: application/json" \
-d "{\"prompt\": $(cat workflow_api.json)}"
# {"prompt_id":"8f2c…","number":0}
curl -s "$APP"history/8f2c…
curl -s -o out.png "$APP""view?filename=ComfyUI_00001_.png&type=output"Text to speech and other Gradio apps
The tts-qwen3, tts-voxcpm and tts-chatterbox templates are built on Gradio. At the bottom of the app page there is a “Use via API” link listing the methods, their parameters and ready examples. Method names differ between apps, so replace /generate in the example with the name from “Use via API” or from view_api().
# pip install gradio_client
from gradio_client import Client
client = Client("http://localhost:7860/") # through an SSH tunnel or the address from the dashboard
client.view_api() # list of methods and their parameters
# method name and argument order: from view_api() or "Use via API"
audio_path = client.predict("Hello! This is a speech test.", api_name="/generate")
print(audio_path) # the result file on your computer// npm install @gradio/client
import { Client } from "@gradio/client";
const client = await Client.connect("http://localhost:7860/");
console.log(await client.view_api());
const res = await client.predict("/generate", ["Hello! This is a speech test."]);
console.log(res.data);# Gradio 5: first the request, then the result by event_id
curl -s -X POST http://localhost:7860/gradio_api/call/generate \
-H "Content-Type: application/json" \
-d '{"data": ["Hello! This is a speech test."]}'
# {"event_id":"3b1f…"}
curl -sN http://localhost:7860/gradio_api/call/generate/3b1f…Chat: Ollama and Open WebUI
Templates chat, chat-pro and chat-mimo: Open WebUI runs on top of Ollama. From a script it is simpler to talk to Ollama directly. It listens only inside the server, on 127.0.0.1:11434, so you need an SSH tunnel as in the example above. The model downloads after launch; until it has loaded, Ollama answers that the model is missing.
import requests
r = requests.post("http://localhost:11434/api/chat", json={
"model": "qwen3:8b", # chat-pro uses qwen3:32b
"messages": [{"role": "user", "content": "In one sentence: what is CUDA?"}],
"stream": False,
})
print(r.json()["message"]["content"])const r = await fetch("http://localhost:11434/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "qwen3:8b",
messages: [{ role: "user", content: "In one sentence: what is CUDA?" }],
stream: false,
}),
});
console.log((await r.json()).message.content);curl -s http://localhost:11434/api/chat -d '{
"model": "qwen3:8b",
"messages": [{"role": "user", "content": "In one sentence: what is CUDA?"}],
"stream": false
}'Ollama also speaks the OpenAI format at http://localhost:11434/v1/chat/completions, so OpenAI libraries work. Open WebUI has its own API too; you issue a key for it in the Open WebUI settings.
vLLM
The vllm template starts an OpenAI-compatible server with Qwen/Qwen2.5-7B-Instruct. The address is access.app, which already ends in /v1. The key is not checked, but the OpenAI library requires a non-empty one, so pass anything. The model list is at GET /v1/models.
# pip install openai
from openai import OpenAI
client = OpenAI(base_url="https://u.vramfarm.com/api/a/<key>/v1", # access.app
api_key="EMPTY") # no key needed
r = client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": "Write a haiku about a graphics card"}],
)
print(r.choices[0].message.content)const BASE = "https://u.vramfarm.com/api/a/<key>/v1"; // access.app
const r = await fetch(`${BASE}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "Qwen/Qwen2.5-7B-Instruct",
messages: [{ role: "user", content: "Write a haiku about a graphics card" }],
}),
});
console.log((await r.json()).choices[0].message.content);BASE="https://u.vramfarm.com/api/a/<key>/v1"
curl -s "$BASE/models"
curl -s "$BASE/chat/completions" -H "Content-Type: application/json" -d '{
"model": "Qwen/Qwen2.5-7B-Instruct",
"messages": [{"role": "user", "content": "Write a haiku about a graphics card"}]
}'Need a token? Sign up, confirm your email and create a token in the dashboard settings. If something doesn’t match this page, or you need a higher limit, write to us.