API Reference
Integrate top-tier GPUs into your application. Simple, fast, and reliable.
Base URL
All endpoints live under the base URL below and respond in JSON:
https://gpubrazil.com
Authentication
Authenticate with an API key in the Authorization header. The key never expires and can be revoked at any time from your dashboard. Treat it like a password — anyone with the key can create and delete instances in your account.
Authorization: Bearer gpub_live_yourkeyhere
Alternatively, you can send the key in the x-api-key header.
Generate your API key in the dashboard
For security, API keys are generated and managed inside your account — in the API Keys section of the dashboard. The plaintext key is shown only once, in the authenticated area.
Open dashboard → API KeysEndpoints
Full workflow
An instance's lifecycle is: choose the GPU → create → check status/connection → (optional) stop/start → delete. In the examples below, set your key in an environment variable:
export GPUB_API_KEY="gpub_live_yourkeyhere"
1. List GPUs and prices
Returns the catalog with the price per hour (in R$) and the gpu_key you use to create the instance. No authentication required.
curl -s "https://gpubrazil.com/api/gpus"
Resposta (resumo):
{
"gpus": [
{ "model": "NVIDIA H100 PCIe 80GB", "gpu_key": "premium_H100-80G-PCIe",
"pricePerHourBrl": 19.88, "provider": "premium" },
{ "model": "RTX 4090", "gpu_key": "economic_RTX_4090",
"pricePerHourBrl": 3.34, "provider": "economic" }
]
}
Tip: use /api/gpus/available to also see the quantity available in real time.
Only the GPUs compatible with a template
1-click templates don't run on every machine: each one needs a minimum amount of video memory, and some only run on a Dedicated virtual machine instance. Add ?template=<id> to /api/gpus and the catalog comes back filtered, by the same rules the deploy applies.
curl -s "https://gpubrasil.com.br/api/gpus?template=llama-factory"
Response (excerpt):
{
"template": { "id": "llama-factory", "name": "LLaMA-Factory",
"minVramGb": 16, "diskGb": 80, "requiresVm": false },
"gpus": [
{ "model": "RTX 4090", "gpu_key": "economic_RTX_4090", "gpuRamGb": 24,
"pricePerHourBrl": 3.34, "templateMinGpuCount": 1 }
]
}
Template ids come from GET /api/templates. templateMinGpuCount is the minimum number of GPUs to request on deploy (the gpuCount field) for the template to fit — 1 for most, more than 1 for large models, which add up the memory across cards. An unknown id returns 400 with TEMPLATE_NOT_FOUND.
Filtering the catalogue
The catalogue has hundreds of entries. Instead of downloading it all and choosing in your code, ask for it already filtered:
# 4090 under R$ 4/h in the Americas, cheapest first curl -s "https://gpubrazil.com/api/gpus?model=4090&max_price_brl=4®ion=NA&sort=price&limit=5" # any card with 80 GB of video memory or more, Dedicated tier curl -s "https://gpubrazil.com/api/gpus?min_vram_gb=80&tier=dedicada&sort=price"
| Parameter | What it does |
|---|---|
tier | dedicada, economica, spot (comma-separated list accepted). |
model / q | Part of the model name (4090, h100). |
max_price_brl / min_price_brl | Hourly price ceiling and floor, in BRL. |
min_vram_gb | Minimum video memory per card. |
region | Macro-region: NA (North America), EU, AP (Asia-Pacific) or GL (any region). |
gpu_count | How many cards you want. Hides models that only ship in larger blocks — so you don't discover the restriction at deploy time. |
available | true keeps only what has a free unit right now. |
supports_templates / video_encoder | Only machines that run 1-click templates / that have a video encoder (streaming and video work). |
sort / order / limit | Sort by price, vram or model; order=desc reverses; limit trims the list. |
With a filter, the response gains count (what is left) and total (the catalogue size). Without filters the format is unchanged. Price and availability move during the day: query close to the moment you create the machine, not once a week.
2. Create an instance
Use the gpuModel = gpu_key obtained in step 1. The deploy runs in the background; the response comes back immediately with an instanceId and status creating.
curl -X POST "https://gpubrazil.com/api/instances/deploy" \
-H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "minha-vm",
"gpuModel": "premium_H100-80G-PCIe",
"vcpuCount": 8,
"ramGb": 64,
"storageGb": 100
}'
Response:
{
"success": true,
"instance": { "instanceId": "abc123", "name": "minha-vm", "status": "creating" },
"chargedAmount": 19.88,
"currency": "R$",
"message": "Instance is being created..."
}
| Field | Default | What it is |
|---|---|---|
gpuModel (required) | — | The gpu_key from the catalogue. |
name | generated | The name you will operate the machine by. Up to 64 characters. See Manage by name. |
gpuCount | 1 | How many cards. Some models only ship in blocks — the catalogue states the minimum in minGpuCount. |
vcpuCount · ramGb · storageGb | 4 · 16 · 100 | Accepted minimums: 2 vCPU, 8 GB RAM, 40 GB disk. |
sshKey or sshKeyName | — | The full public key, or the name of one already registered. This is your way into the machine. |
templateId | — | 1-click template. Use /api/gpus?template=<id> to see where it runs. |
diskId | — | Persistent Disk to attach. Takes the id or name:<disk name>. |
metadata | — | Your labels (flat JSON object): job id, batch, environment. They double as list filters. |
requireUniqueName | false | true refuses creation (409 NAME_IN_USE, no charge) if an active machine already has that name. |
The response returns instanceId immediately — keep it, or use the name you chose. The chargedAmount value is the prepaid first hour: the next charge only happens if the machine outlives it. If provisioning fails, that amount returns to your balance automatically.
3. Status and SSH connection details
Query by instanceId — or by the name you gave it, with name:<name>. When the status becomes running, the connection object provides the IP, port, and a ready-to-use SSH command (already with the correct login user for that machine).
curl -s "https://gpubrazil.com/api/instances/abc123" \
-H "Authorization: Bearer $GPUB_API_KEY"
Response:
{
"id": "abc123",
"name": "minha-vm",
"status": "running",
"gpu_model": "premium_H100-80G-PCIe",
"connection": {
"ip": "203.0.113.42",
"port": 22,
"user": "ubuntu",
"sshCommand": "ssh -i ~/.ssh/your_key -p 22 ubuntu@203.0.113.42"
},
"resources": { "vcpuCount": 8, "ramGb": 64, "storageGb": 100 },
"pricing": { "hourlyRateBrl": 19.88 }
}
4. List and filter your instances
The listing takes filters. If you run dozens of machines at once, ask for only what matters instead of downloading everything and searching in your own code.
# everything
curl -s "https://gpubrazil.com/api/instances" -H "Authorization: Bearer $GPUB_API_KEY"
# only what is running, from the night batch, 10 per page, newest first
curl -s "https://gpubrazil.com/api/instances?status=running&metadata.batch=night&limit=10" \
-H "Authorization: Bearer $GPUB_API_KEY"
Response:
{
"data": [
{
"instance_id": "abc123",
"name": "nightly-train",
"status": "running",
"tier": "economica",
"gpu_model": "RTX_4090@NA",
"gpu_model_label": "RTX 4090 · North America",
"gpu_count": 1,
"ip_address": "203.0.113.42",
"ssh_port": 22,
"ssh_user": "root",
"ssh_key_name": "deploy-ci",
"supports_pause": false,
"disk_persists_on_stop": false,
"price_per_hour_brl": 3.34,
"daily_stopped_brl": 3.94,
"metadata": { "job": "7f21", "batch": "night" },
"template_id": null, "template_port": null, "template_url": null,
"error_message": null, "deleting": false,
"created_at": "2026-09-18 22:14:02"
}
],
"count": 1,
"total": 1,
"limit": 10,
"offset": 0
}
Accepted filters
| Parameter | Example | What it does |
|---|---|---|
name | ?name=nightly-train | Exact name (case-insensitive). |
q / name_contains | ?q=train | Part of the name. Great for pipeline prefixes (?q=job-2026-09). |
status | ?status=running,stopped | One or more statuses, comma-separated. |
tier | ?tier=dedicada,spot | dedicada, economica, spot or cpu. |
gpu | ?gpu=4090 | Part of the GPU model. |
template | ?template=llama-factory | Only machines created with that 1-click template. |
has_ip | ?has_ip=true | Only those that already have an address to connect to. |
metadata.<key> | ?metadata.job=7f21 | Filters by the labels you stored yourself. |
created_after / created_before | ?created_after=2026-09-01 | Creation window (ISO date, UTC). |
sort / order | ?sort=name&order=asc | Sorts by created_at (default), name, price or status. |
limit / offset | ?limit=20&offset=40 | Pagination. limit goes up to 500; without it, everything comes back. |
Filters combine. count is what came in this page and total is how much the filter matched overall — the gap between them is what tells you to paginate. The localInstances field is still in the response with the same content as data, for backwards compatibility; in new code, use data.
Fields worth knowing
| Field | Why it exists |
|---|---|
ssh_user | The login user for that machine, as reported at provisioning. Don't infer it from the machine type: machines in the same tier can have different logins. |
ssh_key_name | Which of your registered keys was installed — the matching private key is the one for ssh -i. |
supports_pause | If false, this machine cannot hibernate: /stop returns 409. To end it, use DELETE. |
disk_persists_on_stop | If false, hibernating discards the disk and the machine comes back blank. Check before stopping. |
template_url | Real address of the template interface, with the port that actually answers (on container machines it is randomised and is not the catalogue one). null while there is no real address yet. |
deleting / status: "deleting" | Cancellation already accepted, waiting for the destruction to finish. Hourly billing has already stopped — do not repeat the DELETE. |
error_message | Plain-language reason for the failure, when status is error. |
5. Manage by name, without listing everything
Every route that takes :instanceId also takes name:<name>. You name the machine at creation and operate it by that name from then on — no need to store the id we generate, and no need to download the whole list to find it before every command.
# create it with a name of your own
curl -X POST "https://gpubrazil.com/api/instances/deploy" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "nightly-train", "gpuModel": "economic_RTX_4090", "requireUniqueName": true }'
# from then on, operate it by name, on any route
H="Authorization: Bearer $GPUB_API_KEY"
curl -s "https://gpubrazil.com/api/instances/name:nightly-train" -H "$H"
curl -s -X POST "https://gpubrazil.com/api/instances/name:nightly-train/stop" -H "$H"
curl -s -X DELETE "https://gpubrazil.com/api/instances/name:nightly-train" -H "$H"
Rules worth knowing before you automate:
| Rule | Why |
|---|---|
The id always wins over the name. Without the name: prefix we look for an id first, and only treat the text as a name if no id matches. | Existing integrations that use ids don't change behaviour, not even if someone names a machine after another machine's id. |
| A name only addresses active machines. | Names repeat over time (a pipeline recreates worker every day). Matching a destroyed machine would send commands into the void. |
Case doesn't matter: name:Train-01 and name:train-01 resolve the same. | Whoever types the name shouldn't have to remember the capitalisation. |
Two active machines with the same name = 409 AMBIGUOUS_NAME, with the list of candidates. We never pick one for you. | "Delete train-01" hitting the wrong machine is damage that cannot be undone. |
HTTP 409 — two active machines with the same name
{
"code": "AMBIGUOUS_NAME",
"error": "There are 2 active instances named \"nightly-train\". Use the instance id (see \"matches\") or rename one of them.",
"matches": [
{ "id": "abc123", "name": "nightly-train", "status": "running", "gpu_model_label": "RTX 4090", "created_at": "2026-09-18 22:14:02" },
{ "id": "def456", "name": "nightly-train", "status": "creating", "gpu_model_label": "RTX 4090", "created_at": "2026-09-19 03:40:11" }
]
}
To keep the name a unique address, send "requireUniqueName": true on deploy: if an active machine already has that name, creation is refused with 409 NAME_IN_USE — nothing is charged — and the response tells you which machine holds the name. Without that field the old behaviour still applies (repeated names are accepted).
6. Rename and label
PATCH /api/instances/:ref fixes the name and stores your own labels (metadata) on the machine — job id, batch, environment, whatever your pipeline needs to find later.
curl -X PATCH "https://gpubrazil.com/api/instances/name:nightly-train" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "nightly-train-v2", "metadata": { "job": "7f21", "batch": "night", "attempt": 2 } }'
Response:
{ "success": true, "instance": { "id": "abc123", "name": "nightly-train-v2",
"metadata": { "job": "7f21", "batch": "night", "attempt": 2 }, "status": "running", ... } }
| Field | Rules |
|---|---|
name | Up to 64 characters: letters, digits, space, dot, @, hyphen and underscore. Cannot start with name: or creating- (addressing prefixes). A name already used by another active machine of yours returns 409 NAME_IN_USE. |
metadata | Flat JSON object: up to 20 keys, values of type string, number or boolean (no nesting), 2 KB total. Send null to clear the labels. |
Renaming is only allowed once the machine actually exists (status running/stopped). During provisioning the answer is 409 RENAME_TOO_EARLY: while the machine is being created the name is still the key we use internally to reconcile it, and changing it there would open a window for the machine to slip outside our billing control. Wait for running — seconds to a few minutes.
Labels come back ready (as an object, not a string) in GET /api/instances, and double as a filter: ?metadata.batch=night.
7. What this machine has cost so far
GET /api/instances/:ref/usage returns the full bill for that instance — including the prepaid first hour, which no hourly usage counter shows.
curl -s "https://gpubrazil.com/api/instances/name:nightly-train/usage" \
-H "Authorization: Bearer $GPUB_API_KEY"
Response:
{
"id": "abc123",
"name": "nightly-train",
"status": "running",
"currency": "BRL",
"hourly_rate_brl": 3.34,
"daily_stopped_brl": 3.94,
"total_charged_brl": 13.36,
"breakdown": { "hourly_usage_brl": 10.02, "reservations_net_brl": 3.34 },
"created_at": "2026-09-18 22:14:02",
"last_charged_at": "2026-09-19 01:14:02",
"age_hours": 4.12
}
| Field | What it is |
|---|---|
total_charged_brl | Everything already taken from your balance for this machine, refunds included. This is the number for a cost report. |
breakdown.hourly_usage_brl | Whole hours already closed and charged. |
breakdown.reservations_net_brl | The hour prepaid at creation (and on each restart), minus refunds. |
daily_stopped_brl | What one day costs with the machine hibernated. It is not 1× the hourly price: the GPU is released, but the disk stays reserved. This is the number that decides between hibernating and destroying. |
last_charged_at | Last hour closed. The next charge happens one hour after this mark. |
Amounts are in BRL, the currency the machine is priced and billed in.
8. Stop and start (optional)
Stopping halts hourly billing, but does not bring the cost to zero: while stopped, the instance is billed a daily fee equivalent to 1 hour of usage. Important: not every machine preserves its disk when stopped — on some Dedicated machines the disk is discarded and the instance comes back blank on start, losing everything installed on it. Check the disk_persists_on_stop field in GET /api/instances before stopping. To end billing entirely, use DELETE.
Not every machine can hibernate: where the card is released on stop, there would be no way to guarantee the same machine back. The supports_pause field says which ones accept it — on the others, /stop returns 409 instead of destroying a machine you didn't ask to destroy.
curl -X POST "https://gpubrazil.com/api/instances/name:my-vm/stop" \ -H "Authorization: Bearer $GPUB_API_KEY" curl -X POST "https://gpubrazil.com/api/instances/name:my-vm/start" \ -H "Authorization: Bearer $GPUB_API_KEY"
9. Delete an instance
Destroys the instance at the provider and ends billing. This is the endpoint you asked about — it exists and it's permanent.
curl -X DELETE "https://gpubrazil.com/api/instances/name:my-vm" \
-H "Authorization: Bearer $GPUB_API_KEY"
Response:
{ "success": true, "message": "Instance removed" }
If the machine is still being created, the destruction may be refused (HTTP 409) — try again in a few minutes. When the answer comes back as 202 (or the instance shows status: "deleting"), the request has been accepted and hourly billing has already stopped: do not repeat the DELETE, the destruction finishes on its own.
Other account resources
Everything below uses the same API key and the same balance. And, as with instances, disks and SSH keys can also be addressed by the name you gave them.
CPU machines (no GPU)
For preprocessing, queues, scraping and services that don't need a video card. These are real dedicated machines, including an option in São Paulo.
# catalogue (public)
curl -s "https://gpubrazil.com/api/cpus"
# create
curl -X POST "https://gpubrazil.com/api/cpus/deploy" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "etl-queue", "cpu_key": "cpu_c3-small-x86@SAO2", "sshKeyName": "deploy-ci",
"metadata": { "batch": "etl" }, "requireUniqueName": true }'
Once created, a CPU machine is listed and operated through the same instance routes (list, status, rename, delete), with tier: "cpu". It is bare metal, so provisioning takes a few minutes — longer than a GPU. An SSH key is mandatory (it is the only way in).
Persistent Disk
Storage that outlives the machine. You attach a disk at creation and the contents of /workspace/disco are synced; attach the same disk to the next machine and your data is back.
# create a 100 GB disk
curl -X POST "https://gpubrazil.com/api/disks" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "training-data", "sizeGb": 100 }'
# list / find by name
curl -s "https://gpubrazil.com/api/disks?name=training-data" -H "Authorization: Bearer $GPUB_API_KEY"
# attach it when creating a machine — by the disk NAME
curl -X POST "https://gpubrazil.com/api/instances/deploy" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "train-01", "gpuModel": "economic_RTX_4090", "diskId": "name:training-data" }'
# rename / delete (deleting destroys the data)
curl -X PATCH "https://gpubrazil.com/api/disks/name:training-data" -H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" -d '{ "name": "training-data-2026" }'
curl -X DELETE "https://gpubrazil.com/api/disks/name:training-data-2026" -H "Authorization: Bearer $GPUB_API_KEY"
A disk is billed by the size you contracted (not by what you use) for as long as it exists, even with no machine attached. Two disks with the same name make any by-name command return 409 AMBIGUOUS_NAME instead of picking one — deleting the wrong disk is data loss that does not come back.
SSH keys
Register the key once and then just refer to it by name on deploy, with sshKeyName — no need to carry the key material inside your pipeline.
curl -X POST "https://gpubrazil.com/api/ssh-keys" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "deploy-ci", "publicKey": "ssh-ed25519 AAAAC3Nza... ci@company" }'
curl -s "https://gpubrazil.com/api/ssh-keys" -H "Authorization: Bearer $GPUB_API_KEY"
# delete by name
curl -X DELETE "https://gpubrazil.com/api/ssh-keys/name:deploy-ci" -H "Authorization: Bearer $GPUB_API_KEY"
On deploy, send sshKey (the full public key) or sshKeyName (the name of a registered one). A name that doesn't exist returns 400 SSH_KEY_NOT_FOUND and creates nothing — a machine you pay for and cannot enter is the worst possible outcome. The instance's ssh_key_name field tells you which key ended up installed.
Balance and statement
curl -s "https://gpubrazil.com/api/user/balance" -H "Authorization: Bearer $GPUB_API_KEY"
{ "balance_brl": 482.15, "user": { "id": 42, "name": "...", "email": "..." } }
# statement, filtered
curl -s "https://gpubrazil.com/api/transactions?type=reservation,usage&limit=100" \
-H "Authorization: Bearer $GPUB_API_KEY"
curl -s "https://gpubrazil.com/api/transactions?q=nightly-train" -H "Authorization: Bearer $GPUB_API_KEY"
| Parameter | What it does |
|---|---|
type | deposit, reservation, usage, refund — comma-separated list accepted. |
q | Searches the entry description (that's where the machine name shows up). |
created_after / created_before | Date window, for closing the month. |
limit / offset | Pagination (default 50, max 500). |
Statement convention: a charge is a negative amount and a credit is positive. Dates are in UTC. The response is a plain array.
Ready-made recipes
Create it, wait until it's ready, connect
Deploy answers immediately with status: "creating"; the machine becomes usable moments later. Poll by name until the status turns running with an IP:
NAME="train-$(date +%s)"
curl -sX POST "https://gpubrazil.com/api/instances/deploy" \
-H "Authorization: Bearer $GPUB_API_KEY" -H "Content-Type: application/json" \
-d "{\"name\":\"$NAME\",\"gpuModel\":\"economic_RTX_4090\",\"sshKeyName\":\"deploy-ci\",\"requireUniqueName\":true}"
# wait up to 15 min, asking every 10 s
for i in $(seq 1 90); do
J=$(curl -s "https://gpubrazil.com/api/instances/name:$NAME" -H "Authorization: Bearer $GPUB_API_KEY")
S=$(echo "$J" | python3 -c "import json,sys; print(json.load(sys.stdin)['status'])")
[ "$S" = "running" ] && echo "$J" | python3 -c "import json,sys; print(json.load(sys.stdin)['connection']['sshCommand'])" && break
[ "$S" = "error" ] && echo "$J" | python3 -c "import json,sys; print('failed:', json.load(sys.stdin)['error_message'])" && break
sleep 10
done
Polling every 5 to 10 seconds is plenty. The connection.sshCommand field comes assembled with the right login user for that machine; just swap in the path to your private key after -i.
Tear down a whole batch by name prefix
Filter by part of the name (or by label) and destroy each one by the id the listing already gave you.
# everything starting with "job-2026-09-19"
curl -s "https://gpubrazil.com/api/instances?q=job-2026-09-19&status=running" \
-H "Authorization: Bearer $GPUB_API_KEY" \
| python3 -c "import json,sys; [print(i['instance_id']) for i in json.load(sys.stdin)['data']]" \
| while read ID; do
curl -sX DELETE "https://gpubrazil.com/api/instances/$ID" -H "Authorization: Bearer $GPUB_API_KEY"
done
The same works by label: ?metadata.batch=night. For batches, prefer deleting by the id from the listing: it is immune to repeated names.
How much each machine has cost
for NAME in $(curl -s "https://gpubrazil.com/api/instances?status=running" \
-H "Authorization: Bearer $GPUB_API_KEY" \
| python3 -c "import json,sys; [print(i['name']) for i in json.load(sys.stdin)['data']]"); do
curl -s "https://gpubrazil.com/api/instances/name:$NAME/usage" -H "Authorization: Bearer $GPUB_API_KEY" \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(f\"{d['name']}: R\$ {d['total_charged_brl']:.2f} ({d['age_hours']:.1f}h)\")"
done
Status vocabulary
| status | What it means | Is it billing? |
|---|---|---|
creating | Provisioning in progress. No IP yet. | Yes — the first hour is prepaid at creation. |
running | Up and usable. connection carries IP, port and user. | Yes, hourly. |
stopped | Hibernated (only where supports_pause is true). | Yes, a storage day rate — see daily_stopped_brl. |
deleting | Cancellation accepted, waiting for destruction to complete. | No. Do not repeat the DELETE. |
error | It failed. The reason is in error_message. | No — whatever was reserved is refunded automatically. |
stopped_no_balance | Shut down for insufficient balance. | Same as stopped. Top up and call /start. |
Usage limits
| Limit | Value | When you hit it |
|---|---|---|
| Machine creation | 40 per 10 minutes, per account | 429, with Retry-After in seconds. It is a guard against retry loops, not a commercial cap: if you need more, just ask. |
| Read requests | no fixed limit | We ask for common sense: 5 to 10 s between polls while a machine boots, and filters instead of listing everything in a loop. |
| Registered SSH keys | 10 per account | 400 when registering the 11th. |
| Disk size | 10 GB to 10 TB | 400 outside that range. |
Inference by the token (OpenAI-compatible)
Not every project needs a whole GPU running. You can also call open-weight models and pay per token consumed, using the same gpub_live_ API key and the same balance in reais that pays for the GPUs. There is no subscription, no monthly fee and no minimum token purchase: billing is proportional to the input and output tokens of each call, and the amount charged comes back inside the response itself.
The API is compatible with OpenAI's. Any SDK, framework or tool that already talks to it works here by changing only the base URL and the key:
https://gpubrasil.com.br/v1
This is the canonical API host, and it serves both languages. We do not use your prompts or your responses to train models. If your use case requires full control over the weights, the logs and the lifecycle of what you send, run the model on a dedicated GPU of your own, with no third-party API in the path.
1. List models and prices
Returns the available models, the context window size and the price per million tokens, in Brazilian reais. Each model has an id — what goes in the model field — a commercial name for display, and a prateleira (shelf).
There are three shelves: fronteira (brand-name models — Claude, GPT and Gemini), confidenciais (running inside a hardware-sealed enclave) and essenciais (open-weight, best price per token). All of them answer through the same OpenAI format, including the ones that would natively require a different request shape.
curl -s "https://gpubrasil.com.br/v1/models" \
-H "Authorization: Bearer $GPUB_API_KEY"
Response (excerpt):
{
"object": "list",
"data": [
{ "id": "gpub-fast", "object": "model", "owned_by": "gpubrasil",
"name": "DeepSeek V4 Flash", "prateleira": "essenciais", "context_length": 1000000,
"pricing": { "currency": "BRL", "inputPerMillion": 0.59, "cachedInputPerMillion": 0.118, "outputPerMillion": 1.29 } },
{ "id": "claude-sonnet-5", "object": "model", "owned_by": "gpubrasil",
"name": "Claude Sonnet 5", "prateleira": "fronteira", "context_length": 1000000,
"pricing": { "currency": "BRL", "inputPerMillion": 20.40, "cachedInputPerMillion": 2.04, "outputPerMillion": 102.00 } },
{ "id": "gpub-selado-nemotron", "object": "model", "owned_by": "gpubrasil",
"name": "Nemotron 3 Nano", "prateleira": "confidenciais", "context_length": 128000,
"pricing": { "currency": "BRL", "inputPerMillion": 0.15, "cachedInputPerMillion": 0.015, "outputPerMillion": 0.60 } }
]
}
The example above is trimmed to three rows, one per shelf; the call returns the whole catalogue. The full, always-current list is at AI models. Need the price table without authenticating (for a pricing page, say)? Use GET /api/inference/models, which is public.
2. Chat completion
The format is identical to OpenAI's. The only difference is the extra usage.cost_brl field: the cost in reais of that call, already computed by the server and already deducted from your balance, so you never have to redo the math on the client.
curl -X POST "https://gpubrasil.com.br/v1/chat/completions" \
-H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpub-plus",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Explain what a GPU is in two sentences." }
],
"max_tokens": 300
}'
Response:
{
"id": "chatcmpl-8f2c1b",
"object": "chat.completion",
"created": 1754870400,
"model": "gpub-plus",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "A GPU is a processor..." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 500,
"completion_tokens": 300,
"total_tokens": 800,
"cost_brl": 0.0085,
"balance_brl_after": 92.15
}
}
The response's model field echoes what you asked for (here, gpub-plus), not the technical id — call it by nickname and the nickname is what comes back. cost_brl (cost of the call) and balance_brl_after (balance after it) are our additions inside the usage object; the X-Request-Id response header identifies the request on our side, so keep it if you ever need to open a ticket. Official SDKs ignore fields they do not know, so these extras break no existing integration. For plain text continuation (no chat roles), the endpoint is POST /v1/completions, with prompt instead of messages.
Image and audio input. Use the standard OpenAI image_url block inside content — it works on every model that accepts images, on any shelf, with no change to your code. For audio, the block is input_audio. If the model you picked does not accept what you sent, the response is 400 with the reason; we never accept the call and silently drop the part the model cannot read. Some models need the image inline (data:<mime>;base64,…) rather than a URL, and the error says when that is the case.
curl -X POST "https://gpubrazil.com/v1/chat/completions" \
-H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 300,
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "What is wrong with this diagram?" },
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KG..." } }
]
}]
}'
3. Streaming
Send "stream": true to receive the answer in chunks, as standard server-sent events. To also receive the token counts and the cost, ask for stream_options: {"include_usage": true}: usage arrives in a frame of its own, right before the final [DONE].
curl -N -X POST "https://gpubrasil.com.br/v1/chat/completions" \
-H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpub-fast",
"messages": [{ "role": "user", "content": "Count to three." }],
"stream": true,
"stream_options": { "include_usage": true }
}'
Response (text/event-stream):
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","model":"gpub-fast","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"One"},"finish_reason":null}]}
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":", two"},"finish_reason":null}]}
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":", three."},"finish_reason":null}]}
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-3a91","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":9,"total_tokens":23,"cost_brl":0.0000155,"balance_brl_after":92.15}}
data: [DONE]
Without stream_options, the usage frame is not sent and you end up without the cost_brl for that call. Consumption is still recorded server-side and shows up in /api/inference/usage. Stop reading when you see data: [DONE], which is not JSON.
4. Official OpenAI SDK
No need to switch libraries. Point the official SDK at our base URL and use your API key; the rest of your code stays the same.
Python (pip install openai)
from openai import OpenAI
client = OpenAI(
base_url="https://gpubrasil.com.br/v1",
api_key="gpub_live_yourkeyhere",
)
r = client.chat.completions.create(
model="gpub-mini",
messages=[{"role": "user", "content": "Hello!"}],
)
print(r.choices[0].message.content)
# cost_brl is an extra field of ours; the Python SDK exposes it in model_extra
print(r.usage.model_extra["cost_brl"])
Node.js (npm i openai)
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://gpubrasil.com.br/v1',
apiKey: process.env.GPUB_API_KEY,
});
const r = await client.chat.completions.create({
model: 'gpub-mini',
messages: [{ role: 'user', content: 'Hello!' }],
});
console.log(r.choices[0].message.content);
console.log(r.usage.cost_brl); // cost in reais of this call
The same applies to any tool that accepts an OpenAI-compatible endpoint: fill in the base URL with https://gpubrasil.com.br/v1 and the key with your gpub_live_.
5. Image generation
Models that generate images have their own surface: POST /v1/images/generations, in the same OpenAI format, with the same key and the same balance. They do not answer on /v1/chat/completions — asking for one there returns 400 pointing here, and vice versa. Each model's endpoint field in GET /v1/models tells you which surface it uses.
curl -X POST "https://gpubrazil.com/v1/images/generations" \
-H "Authorization: Bearer $GPUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpub-imagem",
"prompt": "a tree-lined square at sunrise, watercolor style",
"size": "1024x1024"
}'
Response:
{
"created": 1789807470,
"data": [ { "b64_json": "/9j/4AAQSkZJRgABAQ..." } ]
}
The image comes back as b64_json (base64), ready to write to a file. n must be 1: these models produce one image per call, and asking for more returns 400 before any amount is held from your balance. size accepts a fixed list of sizes — sending one outside it returns 400 with the whole list in the error. Some models do not accept size at all; omit the field for those.
Billing. It depends on the model, and each price is in GET /v1/models: some charge per image (perImage field, in BRL) and others charge per image token (imageOutputPerMillion). The count always comes from what was delivered, never from what was requested — a response with no image is not charged.
Limits, context and balance
Context window. Input and output together must fit within the context_length of the chosen model, which today ranges from 128,000 to 1,000,000 tokens depending on the model. Going over returns 400, with no charge. Each model's value is in GET /v1/models — read it from there instead of hard-coding it, because models get added and windows change.
Response size. max_tokens caps how many tokens the model may generate. Without it, the model decides where to stop within the window, and since output costs more than input on every model, setting a ceiling in production is worth it.
Balance. Before forwarding the call, the server estimates its cost. If your balance does not cover that estimate, the request is refused with 402 and nothing is spent. Top up in the dashboard and try again. Inference over the API is unlocked after your first confirmed deposit: before that the call comes back with 403 and code: "deposit_required" — to try the models without depositing, use the playground in the dashboard. Too many calls in a short window return 429; a momentary outage returns 503.
HTTP 402
{
"error": {
"message": "Insufficient balance to cover the estimated cost of this call.",
"type": "insufficient_quota",
"param": null,
"code": "insufficient_balance"
}
}
Errors follow the OpenAI envelope (error.message, error.type, error.param, error.code), so existing libraries already know how to read them. Note that type and code differ: a short balance comes back as type: "insufficient_quota" with code: "insufficient_balance". Branch on the HTTP status. To track spend and volume, use GET /api/inference/usage?days=30, which returns consumption per day and per model.
Response Codes
200 · OK
Successful request
201 · Created
Instance/resource created
400 · Error
Invalid parameters or insufficient balance
401 · Unauthorized
API key missing, invalid, or revoked
403 · Forbidden
Action not allowed for this credential
402 · Insufficient balance
Balance does not cover the estimated cost of the call
429 · Rate limit
Too many requests in a short window
404 · Not Found
Instance/resource does not exist
409 · Conflict
Instance still being created — try again later
500 · Internal Error
Server error
The code field in errors
Besides the HTTP status, errors from the instance routes carry a stable code — branch on it, not on the message text (which changes with language and wording).
| code | HTTP | What to do |
|---|---|---|
INSTANCE_NOT_FOUND | 404 | That id/name isn't among your active machines. A name only addresses a live machine. |
AMBIGUOUS_NAME | 409 | Two or more active machines share the name. Use one of the ids in matches, or rename. |
NAME_IN_USE | 409 | The name already belongs to another active machine of yours. Nothing was created or charged. |
RENAME_TOO_EARLY | 409 | Wait for the machine to leave creating before renaming. |
INVALID_NAME · INVALID_METADATA | 400 | Name or labels outside the rules. Nothing was created or charged. |
SSH_KEY_NOT_FOUND | 400 | sshKeyName matches none of your keys. See GET /api/ssh-keys. |
DELETE_PENDING | 409 | The machine is already being destroyed; stop/start no longer applies. |
PRICE_CHANGED | 409 | The price moved between your catalogue read and the deploy. Read the catalogue again and retry — we never charge above what you were shown. |
TEMPLATE_NOT_FOUND · TEMPLATE_REQUIRES_GPU | 400 | Unknown template, or one incompatible with the machine you picked. Use /api/gpus?template=<id> to list only what runs it. |
INSUFFICIENT_BALANCE | 402 | Your balance doesn't cover the first hour. The response says how much is missing. |
DISK_NOT_FOUND · DISK_UNSUPPORTED_MACHINE | 404 / 400 | Unknown disk, or a machine type that doesn't take a Persistent Disk yet. |
General rule: every refusal happens before any charge. If the answer was 4xx, nothing left your balance. And if a machine fails after being created, the reservation is refunded automatically — you don't have to ask.