// REST API

REST API

Everything the console, the CLI and the SDK do goes through this API. It's JSON over HTTPS at https://api.matmul.cloud/v1.

Authentication

Create an API key on the CLI & API page of the console and send it as a bearer token:

export MATMUL_API_KEY=lmk_...
curl -s https://api.matmul.cloud/v1/offers/?gpu=H100 \
  -H "Authorization: Bearer $MATMUL_API_KEY"

A key belongs to the person who created it and acts with their role in the organization. The organization comes from the key; there's no org id in any request. The key is shown once. Treat it like a password.

RoleCan
viewerList and read offers, instances, SSH keys and jobs.
memberAlso launch instances and jobs, add and remove SSH keys, and delete their own instances and jobs.
adminAlso delete anyone's instances and jobs.
ownerAlso billing: balance, ledger and checkout. The person who signs up is the owner.

Request bodies are JSON with Content-Type: application/json. Unknown fields are rejected, so a typo fails loudly instead of being ignored. Money is always integer US cents; prices are per hour for the whole machine.

GET /v1/ needs no key and answers {"api": "matmul", "version": "v1"}, so you can check that you're pointed at the right server.

Offers

GET /v1/offers/

Machines you can launch right now, cheapest first. All query parameters are optional.

ParameterMeaning
gpuGPU model, e.g. H100.
countGPUs per machine.
regionOne region.
max_price_centsOnly offers at or under this price per hour.
kindgpu or cpu. Leave it out for both.
{
  "offers": [
    {
      "id": "…",
      "gpu_type": "H100",
      "num_gpus": 1,
      "vram_per_gpu_gb": 80,
      "vcpus": 26,
      "memory_gb": 200,
      "storage_gb": 1000,
      "region": "…",
      "hourly_price_cents": 299,
      "boot_min_sec": 180,
      "boot_max_sec": 600
    }
  ]
}

interconnect, boot_min_sec and boot_max_sec appear only when the provider reports them. The numbers above are an example, not a quote.

SSH keys

EndpointDoes
POST /v1/ssh-keys/Body {"name": "laptop", "public_key": "ssh-ed25519 AAAA…"}. Returns 201 with id (key_...), name, fingerprint, created_at. Up to 20 keys; the same key can't be added twice.
GET /v1/ssh-keys/{"ssh_keys": [...]}
DELETE /v1/ssh-keys/{id}204. Needs the member role or higher, like adding a key.

Instances

POST /v1/instances/

curl -s https://api.matmul.cloud/v1/instances/ \
  -H "Authorization: Bearer $MATMUL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"offer_id": "…", "name": "dev", "ssh_key_id": "key_…",
       "image": "pytorch/pytorch:latest", "keep_alive": true,
       "max_hourly_price_cents": 300}'
FieldMeaning
offer_idRequired. From GET /v1/offers/.
nameRequired. Matches ^[a-z][a-z0-9-]{0,31}$.
ssh_key_idRequired. The key installed on the machine.
max_hourly_price_centsPrice guard: the launch is refused (409) if the live price is above it.
imageDocker image reference. Leave it out for the provider's plain VM (Ubuntu with the NVIDIA driver; no CUDA toolkit).
keep_aliveKeep an interactive image running. Can't be combined with args.
argsContainer arguments: one line, up to 1024 characters.
envsObject of container environment variables, up to 20.
portsContainer ports to publish on the machine, up to 10, not 22.
shared_memory_gb0 to 256.
exposePorts to give public HTTPS links. Not switched on yet.

args, keep_alive, envs, ports and shared_memory_gb need an image. The response is 202 with the instance, status: "provisioning". Poll GET until it's running. With an image, that's once the container is up: the machine stays provisioning while the provider pulls and starts the image, and turns error if the pull or start fails.

GET /v1/instances/ and GET /v1/instances/{id}

{"instances": [...]} for your organization (deleted ones included), or one instance:

{
  "id": "ins_…",
  "name": "dev",
  "status": "running",
  "image": "pytorch/pytorch:latest",
  "gpu_type": "H100",
  "num_gpus": 1,
  "region": "…",
  "hourly_price_cents": 299,
  "ip": "…",
  "ssh_user": "…",
  "ssh_port": 22,
  "ssh_command": "ssh -p 22 <user>@<ip>",
  "container_shell_command": "ssh -t -p 22 <user>@<ip> …",
  "created_at": "2026-09-23T10:00:00Z",
  "created_by": "…",
  "running_seconds": 1260,
  "accrued_cost_cents": 105,
  "ports": [],
  "exposable": false
}

status is provisioning, running, error, deleting or deleted; error carries the reason when there is one. The SSH fields appear once the machine has an address, and container_shell_command only for instances with an image. running_seconds and accrued_cost_cents are display estimates; the ledger is what's billed.

DELETE /v1/instances/{id}

202 with the instance, now deleting. Billing stops at the moment of this request; the status turns deleted once the provider has released the machine. It stops counting toward your instance limit at the same moment, so you can launch a replacement right away. Deleting an instance that's already deleting or deleted is a no-op.

App link endpoints

POST /v1/instances/{id}/ports (body {"port": 8188, "visibility": "public"}), GET /v1/instances/{id}/ports and DELETE /v1/instances/{id}/ports/{port} manage public links. They're built but switched off for now.

Capabilities

GET /v1/capabilities

Which optional features this server has switched on:

{"instances": true, "app_links": false, "jobs": false}

When app_links is on, the response also has app_links_limit (and app_links_blocked_reason when it's 0).

Billing

All billing endpoints need the owner role.

GET /v1/billing/

{
  "balance_cents": 1840,
  "frozen": false,
  "frozen_reason": "",
  "verified": false,
  "limits": {
    "min_topup_cents": 1000,
    "max_topup_cents": 10000,
    "daily_topup_limit_cents": 10000,
    "daily_spend_limit_cents": 5000,
    "max_instances": 1,
    "max_jobs": 1
  },
  "window_24h": {"purchased_24h_cents": 2000, "spent_24h_cents": 160}
}

verified: false means the new-account limits apply. When the account is frozen, the response adds frozen_at and grace_hours_remaining. See Pricing & billing.

GET /v1/billing/ledger

{"entries": [...]}, newest first. limit (default 50, up to 200) and before (an entry id, for the next page). Each entry has id, kind (credit_purchase, usage_charge, adjustment, refund or dispute), amount_cents (negative for charges), created_at, and instance_id or job_id for usage.

POST /v1/billing/checkout

Body {"amount_cents": 2000}. Returns {"checkout_url": "https://checkout.stripe.com/…"}; the credit lands when the payment completes. A top-up outside your limits gets 422 with the reason.

Managed jobs

Not available on MatMul yet: these return 503 jobs are not enabled until they are. Details in Managed jobs.

EndpointDoes
POST /v1/jobs/Launch a job. 202 with the job.
GET /v1/jobs/{"jobs": [...]}
GET /v1/jobs/{ref}One job, by job_... id or by name (the most recent).
GET /v1/jobs/{ref}/logsThe logs, as text/plain.
DELETE /v1/jobs/{ref}Stop the job and release its machine. 202 with the job.
{
  "name": "train",
  "run": "python train.py",
  "setup": "pip install -r requirements.txt",
  "envs": {"WANDB_PROJECT": "demo"},
  "num_nodes": 1,
  "resources": {"accelerators": "H100:1", "cpus": "8+", "memory": "32+", "disk_size": 100},
  "max_hourly_price_cents": 350
}

Errors

Errors are JSON, {"error": "message"}, with a message you can show to a person.

StatusMeans
400The body isn't valid JSON or has an unknown field.
401invalid api key.
403not allowed for your role.
404No such instance, key or job in your organization.
409The offer is gone, the price rose above your guard, or a job name is still in use.
422A field failed validation; the message says which.
429A limit: instance or job count, a frozen account, not enough balance (you need an hour of runway), or GPU capacity is temporarily limited; please try again later when MatMul is at its platform-wide capacity cap (see Pricing & billing).
502The GPU provider failed. Try again.
503The feature isn't switched on for this server.
← All docs