Skip to content

Python SDK

Read and manage your account, workspaces, pods, storage, assets, GPUs and CPU lines, templates, serverless deployments, and machines from Python with the sync Meshive and async AsyncMeshive clients — typed models, price estimates before you spend, idempotent writes, and a structured error hierarchy.

The SDK exposes two clients with identical method names — Meshive (synchronous) and AsyncMeshive (asyncio). Both read the same credentials and configuration as the CLI (see Authentication).

from meshive import Meshive
with Meshive() as client: # reads MESHIVE_API_KEY
me = client.me()
print(me.email, me.user_role)
for ws in client.list_workspaces():
print(ws.namespace_name, ws.status)
detail = client.get_workspace("my-workspace")
print(detail.price_per_hour, detail.gpus)
pods = client.list_pods("my-workspace")
pod = client.get_pod(pods[0].pod_name, "my-workspace")
print(pod.status, pod.raw) # .raw holds the full payload
usage = client.get_pod_metrics(pod.pod_name, "my-workspace")
print(usage.cpu_usage_rate, [g.vram_usage_rate for g in usage.gpus])
# what can I rent right now?
for gpu in client.list_gpus(min_vram=40):
print(gpu.gpu_model, gpu.vram, gpu.price_per_hour, gpu.available_gpus)
credit = client.get_credit()
print(credit.balance)
# host view: the machines you contribute to the network
machines = client.list_machines()
for m in machines:
print(m.machine_id, m.status, m.gpu_count, m.gpu_model)
machine = client.get_machine(machines[0].machine_id)
print(machine.earning_hourly, machine.raw)

Using the client as a context manager (with / async with) ensures the underlying HTTP connection is closed. Otherwise, call client.close() (or await client.close()) yourself.

Meshive(api_key=None, *, base_url=None, timeout=30.0, max_retries=2, headers=None, transport=None)
AsyncMeshive(api_key=None, *, base_url=None, timeout=30.0, max_retries=2, headers=None, transport=None)

With no arguments, credentials resolve in the usual order (explicit argument › environment variable › meshive login file). You can also pass the key explicitly:

client = Meshive(api_key="meshive_xxxxxxxx")

timeout is the per-request timeout in seconds (default 30.0). max_retries controls automatic retries of rate limits, gateway errors and dropped connections (0 disables them). headers adds custom headers to every request (the Authorization and Accept headers cannot be overridden). If no API key can be resolved, the first request raises ConfigurationError.

Write methods send an Idempotency-Key and reuse it on their automatic retries. Save your own idempotency_key= before a write if you need to recover across process restarts. A retained key belongs to the original request: changing its arguments raises MeshiveAPIError (HTTP 422). See Recovering an uncertain write before retrying or choosing a new key.

Creating a client is cheap from 1.7 on, so a server can create one per request, each with that request’s API key. To let those clients reuse connections as well, create one transport when the program starts and pass it to each client with transport= (1.7 or later):

import httpx
from meshive import AsyncMeshive
transport = httpx.AsyncHTTPTransport() # once, at startup
async def handle(api_key):
async with AsyncMeshive(api_key, transport=transport) as client: # per request
return await client.list_workspaces()
# at shutdown
await transport.aclose()

Meshive takes an httpx.HTTPTransport the same way.

  • Only the connections are shared. Each client still sends its own key and headers with every request, keeps its own cookies and applies its own timeout, so nothing one client receives reaches another.
  • The transport is yours. close() and leaving a with block don’t close it; close it yourself when the program ends.
  • The clients wait for one pool. Size it for all of them with httpx.AsyncHTTPTransport(limits=httpx.Limits(...)). Use an async transport from one event loop only.
  • Proxy and certificate settings come from the transport (proxy=, verify=). HTTPS_PROXY and the other proxy variables aren’t read for it.

Sharing saves opening a new connection (and, over HTTPS, a new TLS handshake) for each client. With many requests in flight at once, though, separate clients can use less CPU than one shared pool, so measure before you switch.

Both clients expose the same methods (await them on AsyncMeshive):

MethodReturnsDescription
me()WhoAmIThe current API key’s owner.
list_api_keys()list[ApiKey]Your active API keys (prefixes only).
get_credit()CreditCredit balance and auto-recharge setting.
list_credit_history(*, start_date=None, end_date=None)list[CreditHistoryEntry]Top-ups and refunds (default: last 90 days).
list_workspaces()list[Workspace]Workspaces you can access.
get_workspace(workspace)WorkspaceDetailCost and resource summary of one workspace.
list_members(workspace)list[Member]Members of a workspace.
wait_for_workspace(workspace, *, timeout=180.0, interval=3.0)WorkspacePoll list_workspaces() until the workspace with this ID (namespace_name) has status running, then return it — the point from which you can create pods and tasks in it. A status of error raises MeshiveError at once. Anything else keeps the wait going until timeout: that includes a workspace missing from the list (a new one can be absent for a moment) and terminated (a workspace still being set up can show it for a single poll). On timeout it raises WaitTimeoutError naming the workspace and the last status seen; terminated as the last status means setup failed. Needs 1.6 or later.
list_pods(workspace)list[Pod]Pods in a workspace (by ID).
get_pod(pod_name, workspace)PodA single pod.
wait_for_pod(pod_name, workspace, *, until="running", timeout=600, interval=5)PodPoll until the pod reaches a status. Raises PodCreationFailedError with the reason when the platform cleaned the pod up because creating it failed.
wait_for_new_pod(name, workspace, *, until="running", timeout=600, interval=5, transaction_id=None)PodWait for a pod that create_pod(name, …) just accepted: find it by its display name, then wait like wait_for_pod. create_pod returns before the pod has an ID. Pass transaction_id=created.transaction_id and a creation that fails — for example an image that can’t be pulled — raises PodCreationFailedError with the reason right away instead of running into the timeout. Raises WaitTimeoutError if the pod doesn’t appear or reach until within timeout.
get_pod_metrics(pod_name, workspace)PodMetricsLive CPU/RAM/GPU/disk usage of a pod.
list_transactions(workspace)list[Transaction]Pod operations still in flight — why a pod is still creating. A failed one stays on the list for only a few minutes.
get_transaction(transaction_id, workspace)TransactionOne pod operation by ID, such as create_pod’s transaction_id: its status, steps and failure_reason. There is no time window, so it also finds operations that finished or failed long ago. NotFoundError for an ID outside the workspace.
list_storages(workspace)list[Storage]Storages (volumes) in a workspace.
get_storage(storage_name, workspace)StorageA single storage.
list_gpus(*, rental_type="demand", min_vram=None)list[GpuAvailability]GPU tiers you can rent right now, with prices.
list_cpus(*, rental_type="demand")list[CpuLine]CPU lines you can rent for CPU pods right now, with prices per vCPU and per GiB of RAM. Pass a line’s cpu_line to create_pod.
list_templates(workspace=None, *, app_type=None)list[Template]Official templates, plus a workspace’s custom ones when workspace is given.
get_template(template_id, workspace=None)TemplateA single template (custom ones need their workspace).
list_model_sets(workspace, template_id)ModelSetListThe model sets a template can start with, like the console’s model set tiles: their files, size, minimum VRAM, licenses and workflows. Pass a set’s slug to create_pod(model_set=...) (see Starting a pod with a model set).
list_servings(workspace)list[Serving]Serverless serving deployments in a workspace.
get_serving(serving_id)ServingA single serving deployment.
list_tasks(workspace, *, status=None, limit=50, offset=0)list[Task]Serverless tasks, newest first. status is a string or list of statuses.
get_task(task_id)TaskA single task.
list_assets(workspace, *, asset_type=None, status=None, page=1, page_size=20)AssetPageOne page of a workspace’s assets. Iterate it, or read .items, .total, .pages.
get_asset(asset_id)AssetA single asset with its files and where it is in use.
get_asset_storage(workspace)AssetStorageManaged asset storage usage, cost, and credit status.
import_asset(target, *, workspace, name=None, asset_type=None, revision=None, paths=None, hf_token_id=None, civitai_key_id=None)AssetImportedLink a Hugging Face repo (owner/name or its URL), a CivitAI model URL or a direct file URL as an asset: asset_id, file_count, total_bytes, is_gated, resolved_commit. Nothing is copied — no storage charge, ready at once; pods and tasks download it from the source when they start. Private or gated sources need a token or key saved in the console, by ID.
list_civitai_keys(workspace)list[HfToken]The workspace’s CivitAI keys by token_id and label, for civitai_key_id.
asset_download_urls(asset_id, *, paths=None)AssetDownloadShort-lived download links for an asset’s files: files (DownloadFile(path, url, size_bytes, content_hash)), expires_in seconds, complete. paths takes globs such as "config/*"; one request returns at most 500 files, so narrow a large asset with paths. Linked assets (fetched from Hugging Face or CivitAI when a pod starts) have no links.
download_asset(asset_id, dest, *, paths=None)list[Path]Saves the files under dest, keeping their paths inside the asset, and returns where they went. Existing files are overwritten; a path that would land outside dest is refused.
task_outputs(task_id) / download_task_outputs(task_id, dest)TaskOutputs / list[Path]A task’s output files with download links (files, expired, storage_provider), or saved under dest.
list_machines()list[Machine]Machines you host.
get_machine(machine_id)MachineA single machine.
get_machine_metrics(machine_id)MachineMetricsLive metrics of a machine.
get_earnings(*, start_date=None, end_date=None)EarningsHost earnings summary and daily history.
get_pod_logs(pod_name, workspace, *, tail=200, container=None, wait=None)LogsLast tail lines of a pod’s logs (see Logs).
get_task_logs(task_id, *, tail=200, wait=None, cursor=None)LogsLast tail lines of a task’s logs. For tasks on an external provider, pass a previous result’s next_cursor as cursor to get only the lines after it.
get_operation(operation_id, *, method, path)dictRead the caller’s durable acceptance record. Use the original method and SDK-relative path, such as POST and /tasks, without /v1/sdk or query parameters.

These need a key issued with the Read & write permission. Creations are accepted asynchronously: the returned object carries a transaction_id and the estimate, and the resource appears in the matching list a few seconds later.

MethodReturnsDescription
estimate_workspace(name=None)WorkspaceEstimate(read scope) The name a workspace would get and its price — nothing, as a workspace costs nothing by itself — without creating it. A name you already use is refused (ConflictError Name Taken). Needs 1.6 or later.
create_workspace(name=None, *, idempotency_key=None)WorkspaceCreatedCreate a workspace. name is optional; a blank or missing name generates one. The result’s workspace is the new workspace’s ID. It is not ready yet — call wait_for_workspace before creating anything in it. The name rules and refusals are under Creating a workspace. Needs 1.6 or later.
estimate_workspace_update(workspace, *, name)WorkspaceUpdateEstimate(read scope) The workspace’s current name next to the new one, without renaming anything. A name another of your workspaces already uses is refused (ConflictError Name Taken). Like the change itself, it needs the admin or billing role. Needs 1.6 or later.
update_workspace(workspace, *, name, idempotency_key=None)WorkspaceUpdatedRename a workspace. Its ID doesn’t change, and every member sees the new name. Needs the admin or billing role. The name rules and refusals are under Renaming a workspace. Needs 1.6 or later.
check_workspace_delete(workspace)WorkspaceDeleteCheck(read scope) Whether delete_workspace would go through and, if not, how many of each kind of thing are still in the workspace (blockers). Deletes nothing. Needs the admin or billing role. Needs 1.6 or later.
delete_workspace(workspace, *, idempotency_key=None)ResourceActionDelete an empty workspace. This can’t be undone. A workspace that still holds anything is refused with ConflictError Workspace Not Empty. See Deleting a workspace. Needs the admin or billing role and 1.6 or later.
estimate_pod(name, template_id, *, workspace, ...)PodEstimateHourly price and resolved hardware for a pod, without creating it. Same keyword arguments as create_pod.
create_pod(name, template_id, *, workspace, gpu_model=None, gpu_count=1, gpu_vram_gb=None, rental_type="demand", vcpu=None, ram_gb=None, cpu_line=None, volumes=None, local_volumes=None, env=None, secret_keys=None, ports=None, template_ports=None, command=None, internet_premium=False, uptime_premium=False, cpu_premium=False, region=None, max_price_per_hour=None, input_assets=None, watched_folders=None, watched_template_folders=None, harvest_destination=None, model_set=None, hf_token_id=None, idempotency_key=None)PodCreatedCreate a pod. Omit gpu_model for a CPU pod; cpu_line then picks its CPU line from list_cpus (see CPU pods, local storage and the console’s rules). volumes attaches existing network storage — a list of (pv_name, "/mount") tuples or {"storage": ..., "mount_path": ...} dicts; local_volumes creates new local storage on the pod’s machine together with the pod — a list of {"name", "mount_path", "size_gb"} dicts. ports opens new ports — a list of ints or {"port", "name", "external"} dicts; template_ports renames, renumbers, exposes or hides the ports the template already opens — a list of {"port", "name", "new_port", "external"} dicts. The system disk is sized by the server (see the estimate’s resources["disk_gb"]). region is deprecated and ignored. input_assets places Asset Hub assets in the pod ("asset_id" or {"asset", "target_dir", "role", "paths", "strip_prefix"}; by default under /inputs/<name> or the template’s path for that role — they add an input volume billed by size). paths picks some files or folders of the asset, written as in its file list (a Korean or other accented name matches whether you type it composed or decomposed, NFC or NFD); a path that matches no file is refused; strip_prefix="" keeps their folder structure, like the console’s Keep structure, while leaving it out strips the folder they share. Another key raises ValueError. watched_folders uploads new files in those folders as assets ("path" or {"path", "include", "include_existing", "enabled"}; the template’s output folders are watched already), watched_template_folders sets those template folders from the start, as the console’s create screen does ({"/outputs": {"enabled": False}} turns one off, {"/outputs": {"include": ["*.png"]}} changes its patterns, {"/workspace/output": {"path": "/workspace/results"}} moves an output folder — MESHIVE_SYS_OUTPUT_DIR and its storage follow; 1.4 or later), and harvest_destination={"mode": "user_s3", "credential_id": n} sends them to your bucket. model_set starts the pod with one of the template’s model sets.
update_pod(pod_name, workspace, *, name=None, template_id=None, rental_type=None, gpu_model=None, gpu_vram_gb=None, gpu_count=None, vcpu=None, ram_gb=None, cpu_line=None, env=None, secret_keys=None, remove_env=None, ports=None, template_ports=None, remove_ports=None, command=None, volumes=None, local_volumes=None, detach_storages=None, allow_node_migration=None, allow_data_loss=None, drop_watched_folders=None, max_price_per_hour=None, enable_folder_saving=None, idempotency_key=None)PodUpdatedChange a pod as the console’s edit screen does; only what you pass changes. enable_folder_saving=True (1.4 or later) adds the uploader to an older pod whose get_watched_folders says editable_reason == "no_sidecar" and can_enable_saving. See Editing a pod.
estimate_pod_update(pod_name, workspace, ...)PodUpdateEstimate(read scope) What update_pod would change, whether a running pod restarts, and the hourly price before and after. Same arguments as update_pod; changes nothing.
ssh_access(pod_name, workspace)SshAccessA one-time SSH login, as on the console’s Connect tab: command (the ssh -p … command to run), password, web_url (a browser terminal with the password in it) and expires_at (a few minutes). Needs a read & write key; each call issues a new password. password and web_url are left out of repr.
get_watched_folders(pod_name, workspace)WatchedFoldersA pod’s watched folders: folders (WatchedFolder(path, origin, enabled, include, include_override, since, blocked_reason)), version / revision, editable and editable_reason, applied, restart_hint. folders is None when the server can’t tell right now.
set_watched_folders(pod_name, workspace, *, expected_version, template=None, user=None)WatchedFoldersReplace the whole set without restarting the pod: template = {folder path: {"enabled", "include"}}, user = your folders. Pass the revision you read as expected_version; a 409 means someone else changed it — read again. A folder on network storage can’t be watched; the server’s message says so.
stop_pod(pod_name, workspace)ResourceActionScale the pod to zero (pod billing stops, storage billing continues).
start_pod(pod_name, workspace, *, placement=None, allow_data_loss=False)ResourceActionStart a stopped pod. Leave out placement and the server picks what the console’s Start button would (see Starting a stopped pod); the result’s placement says which one it used. Or choose: same_node starts it on its original machine, any_node on any machine with room; wait_same_node and wait_any_node do the same but, when no machine has room yet, leave the pod waiting and start it by itself as soon as one does. any_node and wait_any_node can move the pod to another machine, which needs separate data-loss consent (see below).
restart_pod(pod_name, workspace)ResourceActionRestart in place.
delete_pod(pod_name, workspace, *, delete_local_storages=None)ResourceActionDelete the pod; local volumes are deleted only when listed.
estimate_storage(name, size_gb, *, workspace, storage_type="nfs", disk_type="NVMe", encrypted=False, region=None, max_price_per_hour=None, usage_warning=None, usage_warning_threshold=None)StorageEstimateHourly price of a network volume.
create_storage(name, size_gb, *, workspace, ...)StorageCreatedCreate a network (nfs) volume that any pod in the workspace can attach (same arguments as the estimate). size_gb must be at least 10 GiB, as in the console. encrypted=True turns on at-rest encryption. usage_warning=True emails you when the volume fills past usage_warning_threshold — a fraction from 0 to 1, 0.9 (90% full) if you leave it out — like the console’s usage warning. Local storage can’t be created on its own — storage_type="hostPath" is refused; create it together with a pod through create_pod(local_volumes=...). disk_type and region are deprecated and ignored: storage is always NVMe.
delete_storage(storage_name, workspace)ResourceActionDelete a volume; refused with ConflictError("Storage In Use") while a pod mounts it.
update_storage(pv_name, workspace, *, name=None, size_gb=None, usage_warning=None, usage_warning_threshold=None, idempotency_key=None)StorageUpdatedRename a volume, grow it or change its usage warning; only what you pass changes. A name another volume in the workspace already uses is refused (ConflictError ‘Name Taken’), as in the console. See Editing and connecting to storage.
estimate_storage_update(pv_name, workspace, ...)StorageUpdateEstimate(read scope) The new size, hourly price and usage warning next to the current ones. Same arguments as update_storage; changes nothing.
connect_storage(pv_name, workspace, *, wait=True, timeout=300.0, interval=3.0, idempotency_key=None)StorageConnectionOpen a volume for SSH and SFTP, as the console’s Connect does, and return a one-time SSH login in ssh. Safe to call again.
get_storage_connection(pv_name, workspace)StorageConnectionWhere a volume’s SSH access stands (phase), without issuing a login.
detect_model(huggingface_repo, *, workspace, hf_token_id=None)ModelDetection(read scope) Whether a Hugging Face repo can be served: status (ok / unsupported / failed), output, framework, context_length, detail, suggested_repo (an original repo to try instead of a GGUF). Creates nothing.
register_model(huggingface_repo, *, workspace, name=None, framework=None, hf_token_id=None, context_length=None)ResourceActionRegister a model for serving; id is the registration ID. Free — the model downloads when deployed. Registering the same repo again returns the existing registration (result["title"] == "Already Registered"). framework is vllm (default) or sglang.
list_models(workspace)list[ServingModel](read scope) Registered models: registration_id, name, huggingface_repo, api_model_id, framework, context_length, …
delete_model(registration_id)ResourceActionRemove a registration. 409 while a serving of it is deployed.
list_hf_tokens(workspace)list[HfToken](read scope) The workspace’s Hugging Face tokens by token_id and label — pass the ID as hf_token_id for a private repo. Tokens are added in the console; the API never returns them.
deploy_serving(model_registration_id, *, workspace, price_cap_per_hour, min_replicas=1, max_replicas=3, autoscale=True, max_context_tokens=None, share_idle_capacity=False)ResourceActionDeploy a registered model as a serving.
scale_serving(serving_id, *, min_replicas=None, max_replicas=None, autoscale=None, price_cap_per_hour=None)ResourceActionChange replica range, autoscaling or the per-replica cap. Serving.scale_raises_cost(...) with the same keywords tells whether the change can raise the hourly cost.
pause_serving(serving_id, *, paused=True)ResourceActionPause (True) or resume (False).
delete_serving(serving_id)ResourceActionDelete a serving.
estimate_task(name, script, *, workspace, image=None, template_id=None, requirements=None, env=None, secret_keys=None, args=None, gpu_model=None, gpu_count=None, gpu_vram_gb=None, cpu_preset=None, max_duration=3600, webhook_url=None, input_assets=None, max_price_per_hour=None, output_include=None)TaskEstimateCompute hourly estimate; the total cost ceiling is unknown. Pass exactly one of gpu_model or cpu_preset. output_include (a glob or a list of them, e.g. ["*.safetensors"]) uploads only the matching files in /outputs — see Choosing which files to save (1.5 or later).
submit_task(name, script, *, workspace, ...)TaskSubmittedSubmit the task (same arguments as the estimate). Use print(..., flush=True) in the script.
stop_task(task_id)ResourceActionStop a queued or running task.

Money comes back as the server’s unrounded value — pod.price_per_hour is "0.06770833", not a rounded display string — so your own arithmetic stays exact. To print it the way the console and CLI do, use meshive.format_hourly(value) for an hourly rate (three decimals) and meshive.format_usd(value) for any other amount (two).

Every write method also accepts idempotency_key= (see Constructing a client). Malformed arguments — an update_pod or update_storage call with nothing to change, a blank name for update_workspace, a GPU count below 1, a relative mount path, a local_volumes entry without name, mount_path or size_gb, a template_ports entry without port, a secret_keys entry that is not in env, a task duration outside 1–24 hours, a script above 256 KiB — raise ValueError before any request is sent. Which values are allowed — GPU counts, vCPU and RAM sizes, placements, storage types and sizes — is up to the server, so new values work without upgrading the SDK, and a refused value comes back as an API error that says what is allowed.

Deprecated arguments — region on pods and storage, disk_type on storage — are still accepted but ignored. They raise a DeprecationWarning and will be removed in 2.0.

CPU pods, local storage and the console’s rules

Section titled “CPU pods, local storage and the console’s rules”

Pod creation follows the same rules as the console. A request that breaks one is refused with a MeshiveAPIError before anything is created: .title names the rule (for example vCPU Not Offered, Port In Use or Required Env Missing), .message says what to change, and .raw["detail"] carries extras such as the allowed values.

SettingRule
vcpu, ram_gbMust be sizes the console offers for that hardware. Leave them out for the default: the GPU’s recommended sizes, or for a CPU pod 8 vCPU and 32 GiB of RAM (the nearest sizes its line offers).
cpu_lineCPU pods only. A cpu_line from list_cpus(); without it, the cheapest line that fits is used.
volumesExisting network (nfs) storage only. Local storage that already exists can’t be attached to a new pod.
local_volumesNew local storage on the pod’s machine, created together with the pod: {"name", "mount_path", "size_gb"}, optionally with "encrypted", "usage_warning" and "usage_warning_threshold" (0–1). At least 10 GiB each; not available for spot pods.
portsNew ports only: not a port or port name the template already opens, and no port twice. To change a port the template opens, use template_ports.
template_portsChanges the ports the template opens, as the console’s deploy screen does: [{"port": 8080, "name": "ide", "new_port": 8443, "external": False}]. port is the template’s port as it is now; name renames it, new_port gives it another number and external exposes it (True) or makes it internal only (False) — each only when given. Template ports can’t be removed. A port the template doesn’t open is refused (Port Not In Template), and so is a clash with another port or name (Port In Use, Port Name In Use).
envEnvironment variables the template requires need a value.
commandAt most 4,096 characters.

New local storage is billed hourly by size until it is deleted — also while the pod is stopped, and after the pod is deleted unless you name it in delete_pod(..., delete_local_storages=[...]). The estimate includes it as breakdown["local_storage"].

with Meshive() as client:
ws = "my-workspace-id"
template_id = ... # a template ID from list_templates()
lines = client.list_cpus()
for line in lines:
print(line.cpu_line, line.vendor, line.product_line, line.price_per_core, line.ram_price_per_gb)
# A CPU pod (no gpu_model) on the first line, with 50 GiB of new local storage
est = client.estimate_pod("cpu-box", template_id, workspace=ws, cpu_line=lines[0].cpu_line,
local_volumes=[{"name": "scratch", "mount_path": "/scratch", "size_gb": 50}])
print(est.price_per_hour, est.resources["vcpu"], est.resources["ram_gb"], est.breakdown.get("local_storage"))

For pods and tasks, max_price_per_hour caps the final compute hourly rate, including CPU/RAM charges. Existing volumes you attach, automatically created volumes and Asset Hub retention are billed separately. Before a pod is created, the cap is also checked against its estimate, which includes any new local_volumes. The server checks the cap again before placement; an over-cap pod can fail asynchronously after the API accepted its creation. Read the resource state and final rate rather than treating acceptance as successful placement. A capped CPU request is refused when the server cannot quote its rate.

For storage, the cap applies to that volume’s final initial hourly rate. Task max_duration bounds script runtime; input fetching can add compute time, and retained storage can outlive the task. max_cost is therefore unknown, including for GPU tasks.

Before start_pod(..., placement="any_node") or placement="wait_any_node", inspect pod.has_unpreserved_workspace. Both can move the pod to another machine: unpreserved workspace files can then be permanently deleted, and attached local storage stays on the old machine. Set allow_data_loss=True only after separate consent for that pod and move; without it the server refuses with ConflictError (Data Loss Consent Required). The same applies when the server picks one of these placements for you (see Starting a stopped pod). pod.storage_rate_per_hour reports storage separately from compute.

Without a placement, start_pod lets the server pick what the console’s Start button would, from the pod’s state:

The podPlacement used
Is waiting for any machine (waiting_mode is any_node)wait_any_node
Can no longer run on its original machine (same_node_unavailable_reason is spec_mismatch)any_node, or wait_any_node if no machine has room right now
Anything elsesame_node, or wait_same_node if its original machine has no room right now

The result’s placement says which one was used. Before 1.1 the default was always same_node; pass placement="same_node" to keep that.

A pick that moves the pod needs the same consent as asking for the move yourself. When the pod may have unpreserved workspace files, the start is refused with ConflictError (Data Loss Consent Required) and nothing is started; err.raw["detail"]["placement"] names the placement. After separate consent, call again with that placement and allow_data_loss=True:

from meshive import Meshive, ConflictError
with Meshive() as client:
ws = "my-workspace-id"
pod_name = ... # a stopped pod's ID
try:
action = client.start_pod(pod_name, ws)
print("starting with", action.placement) # e.g. same_node or wait_same_node
except ConflictError as err:
if err.title != "Data Loss Consent Required":
raise
placement = err.raw["detail"]["placement"] # e.g. any_node
# Show the loss warning and ask; only after an explicit yes:
action = client.start_pod(pod_name, ws, placement=placement, allow_data_loss=True)

When you choose same_node or any_node and no machine can take the pod right now, start_pod raises ConflictError (No Available Node). Choose wait_same_node or wait_any_node to have it wait for room instead; without a placement, the server picks a waiting placement when it sees no room. If pod.same_node_unavailable_reason is spec_mismatch, the pod’s saved hardware no longer matches its original machine, so same_node and wait_same_node are refused — use any_node or wait_any_node, or leave out placement.

update_pod changes an existing pod the way the console’s edit screen does. Pass only what changes; everything else stays as it is. estimate_pod_update takes the same arguments and changes nothing: it returns the changes in words, the hourly price before and after, and whether the pod restarts.

To changePass
Namename. A rename alone applies in place without a restart (renamed_only is True).
Templatetemplate_id. The pod’s variables, ports and command then start from the new template; the estimate’s changes list what you added that doesn’t carry over.
Hardwaregpu_model, gpu_vram_gb, gpu_count for a GPU pod, cpu_line for a CPU pod, rental_type, and vcpu / ram_gb (pod totals, in sizes the console offers). Changing the GPU resets vCPU and RAM per GPU to the new GPU’s recommended sizes; changing only gpu_count keeps the per-GPU ratio; changing the CPU line resets cores and RAM to the console’s default.
Environmentenv sets or adds variables (secret_keys marks secret ones); remove_env removes variables you added.
Portsports opens new ports, template_ports renames, renumbers, exposes or hides current ones (as in create_pod), and remove_ports closes ports you added.
Commandcommand replaces the start command; "" goes back to the template’s.
Storagevolumes attaches existing network storage, or local storage on the pod’s current machine; local_volumes creates new local storage there (as in create_pod); detach_storages detaches volumes by ID — the volume and its data stay. Spot pods can’t have local storage, so detach it in the same call to switch to spot.

The pod type (GPU or CPU), premiums and image can’t change, and the template’s own variables and ports can’t be removed — set another value, or rename, renumber or hide the port instead. Such a request is refused with a MeshiveAPIError whose .title names the rule (for example Pod Type Locked or Nothing To Change). max_price_per_hour refuses the edit when the new estimate is above it.

A running pod restarts with the new settings; a stopped pod uses them at its next start. Input assets and watched folders are kept. Follow the change with get_transaction(updated.transaction_id, workspace) (action update).

Some edits need consent first; nothing changes until you call again with it:

RefusalMeaning and next step
ConflictError Node Migration RequiredThe pod’s machine can’t fit the change. With the user’s agreement, call again with allow_node_migration=True to let the pod move to another machine.
ConflictError Data Loss Consent RequiredThe move would permanently delete the pod’s unpreserved workspace files (pod.has_unpreserved_workspace). After separate consent, add allow_data_loss=True.
Cannot Change ResourcesThe pod has local storage attached, which keeps it on its machine, and that machine can’t fit the change. Detach the local storage (detach_storages) or keep the current resources.
MeshiveAPIError with raw["detail"]["code"] == "harvest_roots_orphaned"The change removes storage that holds watched folders (raw["detail"]["paths"]). After the user agrees, pass those paths as drop_watched_folders.
ConflictError Resource values need confirmationAn older pod: pass ram_gb (and vcpu) with the change.
from meshive import Meshive, ConflictError
with Meshive() as client:
ws = "my-workspace-id"
pod_name = ... # the pod's ID
est = client.estimate_pod_update(pod_name, ws, gpu_count=2)
print(est.changes, est.current_price_per_hour, "->", est.price_per_hour, "restarts:", est.restarts)
try:
updated = client.update_pod(pod_name, ws, gpu_count=2)
except ConflictError as err:
if err.title != "Node Migration Required":
raise
# Only after the user agreed to the move, and separately to losing unpreserved files:
updated = client.update_pod(pod_name, ws, gpu_count=2,
allow_node_migration=True, allow_data_loss=True)
if updated.transaction_id is not None:
print(client.get_transaction(updated.transaction_id, ws).status)

update_storage renames a volume (name), grows it (size_gb) or turns its usage warning on or off (usage_warning, usage_warning_threshold, as in create_storage). Storage can grow but never shrink — a smaller size_gb is refused with MeshiveAPIError Storage Cannot Shrink (raw["detail"]["currentSizeGb"] is the current size) — and the size is at least 10 GiB. Growing is billed at the new size from then on and runs as a transaction; a rename or a usage warning change applies at once. Check the new price with estimate_storage_update first.

connect_storage opens a volume for SSH and SFTP, like the console’s Connect. It waits until the volume is ready (polling every interval seconds, up to timeout) and fills ssh with a one-time SSH command and password, as ssh_access does for a pod. Encrypted storage is unlocked for this and locks again automatically after a while (auto_lock_minutes, remaining_seconds). With wait=False it returns the status as it is, with ssh filled only if the volume is ready already. It raises StorageConnectionError when the volume can’t be prepared and WaitTimeoutError when it isn’t ready in time — preparation continues, so call it again later.

with Meshive() as client:
ws = "my-workspace-id"
pv = ... # a storage ID from list_storages()
est = client.estimate_storage_update(pv, ws, size_gb=100)
print(est.current_size_gb, "->", est.size_gb, "GiB,", est.current_price_per_hour, "->", est.price_per_hour)
client.update_storage(pv, ws, size_gb=100)
conn = client.connect_storage(pv, ws)
print(conn.ssh.command) # conn.ssh.password is the one-time password; don't log it

A model set is a ready-made bundle of model files that a template offers, like the console’s model set tiles (see Bundled ComfyUI workflows). list_model_sets(workspace, template_id) lists them; pass one slug as model_set to estimate_pod and create_pod. A pod takes one set, and the estimate describes it in model_set (file count, size and minimum GPU memory).

with Meshive() as client:
ws = "my-workspace-id"
template_id = ... # e.g. a ComfyUI template from list_templates()
sets = client.list_model_sets(ws, template_id)
if not sets.selectable:
raise SystemExit(sets.reason) # this template takes no model set
for s in sets:
print(s.slug, s.name, len(s.items), s.total_size_bytes, s.min_vram_gb)
est = client.estimate_pod("comfy", template_id, workspace=ws, gpu_model="RTX 4090",
model_set=sets.model_sets[0].slug)
print(est.price_per_hour, est.model_set)
  • Files come first. The set’s files are added to the workspace before the pod is created, which can take several minutes. create_pod with model_set therefore waits up to 15 minutes for the answer and is not resent automatically after a timeout. After a timeout the pod may still be created: check list_pods, or get_operation(err.idempotency_key, method="POST", path="/pods"), before trying again. A retry with the same key while the first request is still running is refused with ConflictError Operation Outcome Unknown.
  • All or nothing. If any file can’t be added, no pod is created: MeshiveAPIError Model Set Unavailable lists the failed files in raw["detail"]["failed"]. Files already added stay in the workspace and are reused next time.
  • Which templates. A template that adds its own set automatically, or has no model folders, can’t take one (selectable is False, with a reason). A slug the template doesn’t offer is refused with Model Set Not For Template, and raw["detail"]["available"] lists the sets that fit.
  • Gated sets (1.3+). A set with hf_token_required comes from Hugging Face repos that ask you to accept a license first, such as FLUX 3 Image Klein. Its files download with your own token: accept the license on the page in gated_sources with your Hugging Face account, then create the pod from the set’s Quick Deploy template with hf_token_id — an id from list_hf_tokens(workspace) for a token of that account. The refusals say what to fix: Hugging Face token required, License not accepted / Access not approved (the token’s account hasn’t accepted the license, or the publisher hasn’t approved it yet) and Hugging Face token rejected. A gated set passed as model_set on another template is refused with Model Set Needs a Token. See Gated model sets.

Keep the key, request arguments and returned resource/transaction IDs. Success results expose raw["idempotencyKey"], raw["operationMethod"] and raw["operationPath"]. Terminal API/network exceptions expose idempotency_key, operation_method and operation_path when a write was sent.

# Read-only lookup: use the key saved before the original submit_task call.
with Meshive() as client:
operation = client.get_operation(saved_key, method="POST", path="/tasks")
print(operation)
if operation.get("taskId"):
print(client.get_task(operation["taskId"]).status)
StateMeaning and next action
doneThe API response was saved. Repeating the identical request with the same key replays it; check the task/pod/serving/storage separately for completion.
pending / unknownThe request is fenced against duplicate execution. Reconcile the recorded task/transaction; do not submit a new key or assume waiting will unlock it.
not_foundNo record was found for this caller, key, method and path. Verify all four and inspect resource state; absence alone does not prove an earlier timed-out request did nothing.

There is no automatic expiry or takeover of retained records. An explicit refusal before work was committed (for example No Capacity or Scheduling Busy) can release the key. Retry the same request with the same key after resolving that condition, respecting Retry-After. Use a new key for changed work only after resolving the original outcome.

create_workspace makes a new, empty workspace and follows the console’s rules for its name. The name is optional, at most 63 characters, without control characters, and different from every workspace name you already use — an exact match against all of them, whatever their status, so Research and research are different names. Leading and trailing spaces are removed, and a blank or missing name generates one starting with my-workspace-. An empty workspace costs nothing; pods, storage and tasks in it are billed as usual. A key is not tied to one workspace, so your API key works in the new one, and every workspace you are the billing owner of draws on the same credit.

RefusalMeaning and next step
ConflictError Name TakenOne of your workspaces already has that name. Choose another, or leave the name out.
MeshiveAPIError (HTTP 422)The name is longer than 63 characters or has control characters.
MeshiveAPIError (HTTP 503) No Cluster AvailableNo cluster can take a new workspace right now. The client has already waited and retried, so wait at least 30 seconds before calling again.

A refused call creates nothing. Calling again with the same idempotency_key after a successful create returns the first response instead of failing with Name Taken; a new key sees the name as taken.

The call returns once the request is accepted, before the workspace can be used. The status in list_workspaces() is the only signal (get_workspace has none): pods and tasks are refused until it is running (Workspace not found or not ready, Workspace is not ready), and storage or a serving may be accepted and then fail, so wait for running before creating anything. wait_for_workspace does that. Right after creation a workspace can be missing from the list for a moment, and can show terminated for a single poll while it is being set up, so neither ends the wait; error does.

from meshive import Meshive, ConflictError, WaitTimeoutError
with Meshive() as client: # a Read & write key
est = client.estimate_workspace("research") # refuses a name you already use; creates nothing
print(est.name, est.price_per_hour) # a workspace costs nothing by itself
try:
created = client.create_workspace("research")
except ConflictError as err: # Name Taken
raise SystemExit(f"{err.title}: {err.message}")
try:
ws = client.wait_for_workspace(created.workspace) # returns once it is running
except WaitTimeoutError as err: # names the last status it saw
raise SystemExit(str(err))
print(ws.namespace_name, ws.status) # now create pods, tasks and storage in ws.namespace_name

update_workspace changes a workspace’s display name and nothing else. Its ID (namespace_name) stays the same, so everything that addresses the workspace by ID keeps working, and every member sees the new name. It needs the admin or billing role in the workspace; a viewer gets PermissionDeniedError.

The name follows the rules of create_workspace: at most 63 characters, no control characters, leading and trailing spaces removed. Here a blank name never means “generate one”: it raises ValueError before any request is sent. The name must also differ from the name of every other workspace you belong to — an exact match, so Research and research are different names. A workspace you have deleted no longer counts. Renaming a workspace to the name it already has succeeds and changes nothing.

RefusalMeaning and next step
ConflictError Name TakenAnother of your workspaces already has that name. Choose another.
MeshiveAPIError (HTTP 422)The name is longer than 63 characters or has control characters.
PermissionDeniedErrorThe key is read only, your role in the workspace is viewer, or you aren’t a member of it. A wrong workspace ID is refused the same way.
NotFoundError Workspace Not FoundThe workspace is already being deleted.

estimate_workspace_update runs the same checks without renaming anything, so use it to preview a change. The rename is applied at once and updated.name is the name the server confirmed, but list_workspaces() can take a moment to show it — keep using the ID right after. Calling update_workspace again with the same idempotency_key returns the first response.

from meshive import Meshive, ConflictError
with Meshive() as client: # a Read & write key, admin or billing role
ws = "a1b2c3d4e5f60718" # the workspace ID, not its name
est = client.estimate_workspace_update(ws, name="research-2")
print(est.current_name, "->", est.name) # changes nothing
try:
updated = client.update_workspace(ws, name="research-2")
except ConflictError as err: # Name Taken
raise SystemExit(f"{err.title}: {err.message}")
print(updated.workspace, updated.name) # the same ID, the new name

delete_workspace deletes a workspace that is empty, and only that. It can’t be undone. It needs a Read & write key and the admin or billing role in the workspace. A workspace that still holds anything is refused with ConflictError Workspace Not Empty, and err.raw["detail"]["blockers"] counts what is left. check_workspace_delete runs the same check without deleting, so you can read the list first.

What counts as not empty. blockers lists only the kinds that have something in them, each with how many. All of these have to be cleared first:

blockers keyWhat countsHow to clear it
podsPods that aren’t deleted, stopped ones included. This also counts serving replicas and system pods: every storage volume that isn’t encrypted has one.Delete your pods (delete_pod). Replicas go with their serving (delete_serving), and a volume’s system pod goes with the volume (delete_storage). A system pod can’t be deleted on its own.
storagesStorage volumes that aren’t deleted, network or local. A local volume stays, and bills, until you delete it, unless you named it in delete_pod(..., delete_local_storages=[...]). For a few minutes after a pod is gone, this count can also include a temporary volume the platform made for it, which list_storages doesn’t show and which goes away by itself.Delete the volumes (delete_storage).
servingsServing deployments, paused ones included.Delete them (delete_serving).
tasksTasks that are queued or running, and finished tasks that are still in your Tasks list.Stop running tasks (stop_task). Delete finished ones from the Tasks page in the console — see Tasks.
task_outputsTasks, including ones you removed from the list, whose outputs haven’t finished being saved. A finished task can count for about 30 minutes after it ended, even if it wrote no files.Nothing to do: it clears on its own, about 30 minutes after the task ended or when its upload finishes. A task that is still uploading stays in your Tasks list, where you can follow or discard it. If it is still there after that, look at the task in the console or contact support.
assetsAssets in Asset Hub: uploaded, imported, linked, harvested and task outputs. Assets under abuse review (frozen) count too.Delete them in the console. An asset under review can’t be deleted until the review ends.
templatesYour workspace’s custom templates (not the official ones).Delete them in the console.
modelsModels registered for serving (not the official ones).delete_model, or the console.
credentialsSaved Hugging Face tokens, CivitAI keys, object-storage credentials and private-registry logins.Remove them in the workspace’s settings in the console.
webhooksWebhook endpoints.Remove them in the console.
inference_keysThe workspace’s active keys for the serverless inference API (mk-…).Remove them in the workspace’s settings in the console.
membersMembers other than you. They get no notice when a workspace is deleted, which is why they count.Remove them in the workspace’s settings in the console. The only billing member can’t be removed: ask them to run the delete, or take the billing role first in the Team settings, which makes them an admin.
invitationsInvitations that haven’t been accepted and were sent in the last day — the ones the console lists. Accepting one after the delete would add a member to a workspace that no longer exists.Cancel them in the console.
pending_payoutsEarnings from sharing a serving’s idle capacity that haven’t been credited to your balance yet. They are paid to the workspace’s billing owner, so a delete would lose them.Clears once the earnings are paid out, usually within a day. Earnings on hold, or a failed payout, can take longer: contact support. Deleting in the console before that loses them.
operations_in_progressPod, storage or workspace operations that are still running, and a workspace that is still being set up after you created it.Nothing to do: it clears on its own, usually within minutes, at most about 30 minutes. A workspace that is still being set up stops counting once it is running or has failed.

The server may add kinds in later releases, so treat the keys as open-ended. A freshly created workspace has none of these once its setup has finished.

What doesn’t count. These never stop a delete: short-lived serverless inference results (they expire on their normal schedule), billing history, your own meshive_… API keys (they belong to your account, not to a workspace), files in your own object-storage bucket (Meshive never deletes them), links added when you attach a model set (the ”+ N assets from model sets” entry on the Assets page; Meshive stores none of those files), and anything you have already deleted — pods, storage, servings and tasks removed from your list, once a removed task’s outputs have finished saving (see task_outputs).

A workspace that still holds things. No API key or tool can delete one; a person can, in the console, which removes what’s in it for good. Once no pods, storage or servings remain, the console’s ordinary Delete goes through and takes the assets, tasks, credentials and members with it, without listing them. While any of those three remain, it offers Delete everything instead. Either way the person confirms by typing the workspace name, and the files in managed storage are removed shortly after, for good. The check is what stops a leaked key from doing that. It doesn’t make a write key harmless: a key can still delete pods, storage volumes and servings one at a time.

What a delete removes. Because the workspace has to be empty, little is left to remove besides the workspace itself:

  • The workspace, for good: its name, its settings and your access to it. There is no undo and nothing to restore.
  • Whatever could only be reached through the workspace: the history of pods, storage and servings you had already deleted, tasks you had removed from your list, short-lived inference results and the workspace’s cost view. Charges already made stay charged.

Your credit balance, your API keys and the files in your own bucket are not touched.

The name. The deletion runs in the background. The workspace leaves your list within a moment, and its name stays taken for a while: creating a workspace with it is refused with Name Taken until the deletion has finished. If you deleted tasks, or a serving running on external capacity, just before, their charges are settled first and the name can stay taken for up to about a day.

Running it again. Calling delete_workspace again with the same idempotency_key after it succeeded returns the first response. A new key finds nothing to delete and is refused (NotFoundError or PermissionDeniedError). The check and the delete are separate steps, so don’t add anything to a workspace while you delete it. The key’s owner gets an email naming the key and where the request came from (see Authentication).

RefusalMeaning and next step
ConflictError Workspace Not EmptySomething is still in the workspace. err.raw["detail"]["blockers"] counts it; clear it using the table above, then call again.
MeshiveAPIError (HTTP 400) Unable to delete workspaceA protected workspace, or something was added after the check. check_workspace_delete shows what is left.
PermissionDeniedErrorThe key is read only, your role in the workspace is viewer, or you aren’t a member of it. A wrong workspace ID is refused the same way.
NotFoundError Workspace Not FoundThe workspace is already being deleted.
from meshive import Meshive, ConflictError
with Meshive() as client: # a Read & write key, admin or billing role
check = client.check_workspace_delete("a1b2c3d4e5f60718")
if not check.deletable: # e.g. {"pods": 2, "assets": 14}
raise SystemExit(f"'{check.name}' still holds: {check.blockers}")
try:
client.delete_workspace(check.workspace)
except ConflictError as err: # e.g. Workspace Not Empty: something was added since the check
raise SystemExit(f"{err.title}: {err.message}")
import uuid
from meshive import Meshive, ConflictError, PodCreationFailedError
with Meshive() as client: # a Read & write key
ws = "my-workspace-id"
name = "train-" + uuid.uuid4().hex[:12]
est = client.estimate_pod(name, 457, workspace=ws, gpu_model="RTX 3060")
print(f"${est.price_per_hour}/h on {est.resources['gpu_model']}")
create_key = str(uuid.uuid4()) # save durably before sending in an application
print("create operation:", create_key)
try:
created = client.create_pod(name, 457, workspace=ws, gpu_model="RTX 3060",
max_price_per_hour=0.10, idempotency_key=create_key)
except ConflictError as err: # No Capacity / Name Taken / Price Exceeds Cap
raise SystemExit(f"{err.title}: {err.message}")
# Use a unique display name for this operation; wait_for_new_pod finds the pod by it.
# With transaction_id, a failed creation ends the wait at once with the reason.
try:
pod = client.wait_for_new_pod(created.name, ws, until="running",
transaction_id=created.transaction_id)
except PodCreationFailedError as err: # creating it failed — fix the cause, don't retry this pod
raise SystemExit(err.reason)
print(client.get_pod_logs(pod.pod_name, ws, tail=20).text)
client.stop_pod(pod.pod_name, ws)
client.wait_for_pod(pod.pod_name, ws, until="stopped")
client.delete_pod(pod.pod_name, ws)

Responses are parsed into lightweight dataclasses. Frequently used scalar fields are typed; deeply nested structures aren’t enumerated — the full original payload is preserved on .raw, so the SDK keeps working even when the backend adds fields.

FieldTypeNotes
emailstr
usernamestr | None
user_rolestr
rawdictFull payload.
FieldTypeNotes
namespace_namestrID — pass to list_pods / get_pod.
workspace_namestrDisplay label.
descriptionstr
member_countint
statusstrrunning when the workspace is ready for pods and tasks. A workspace you just created isn’t until it gets there — see wait_for_workspace.
price_per_hourstr
resourcesWorkspaceResources.pod, .storage, .serverless counts. Includes stopped pods and paused deployments.
member_rolestr | NoneYour role here: admin, billing or viewer. A viewer key gets 403 on every change.
created_at / updated_atdatetime | None
rawdictFull payload.

To count only what a workspace is actually running, read ws.raw["activeResources"] — same three keys, but pods you stopped and deployments you paused are left out. A typed field will follow in a later SDK release.

FieldTypeNotes
pod_namestrID — pass to get_pod.
namespace_namestrOwning workspace ID.
user_aliasstrDisplay label.
statusstr
rental_typestrspot / demand.
price_per_hourstr
storage_rate_per_hourstrStorage rate, separate from compute.
has_unpreserved_workspacebool | NoneWhether a node move may lose workspace files. None means unknown; do not assume preservation.
is_maintenanceboolTrue while the resource’s host machine has an open maintenance window — the resource isn’t billed then.
created_atdatetime | None
endpointslist[PodEndpoint]The pod’s URLs, as on the console’s Connect tab: name, port, port_type, external_url, internal_url, is_external, readiness (ready / preparing / interrupted).
connect_credentialslist[ConnectCredential]The logins the console shows on Connect, such as the ACCESS_PASSWORD a ComfyUI pod generates: key, value, is_secret, is_auto_generated. value is left out of repr; don’t log it. A read-only API key gets *** for secret values; a key with write scope gets them.
billing_activebool | NoneWhether compute is being billed now. False while the host machine has an open maintenance window. A stopped pod’s storage is billed separately.
stop_reason_code / stop_reason_detail / stop_reason_atWhy a stopped pod stopped. None or user is a normal stop; anything else means the system stopped it.
same_node_unavailable_reasonstr | NoneWhy a stopped or waiting pod can’t start on its original machine now: spec_mismatch (refused — start it with any_node or wait_any_node), hardware_changed, node_unavailable, requirement_unmet, capacity.
can_restart_on_same_node / can_restart_on_any_nodebool | NoneComputed for stopped pods.
waiting_modestr | Nonesame_node or any_node while the pod is waiting.
container_running_atdatetime | NoneWhen the container started running.
cpu_premium / uptime_premium / internet_premiumbool | NonePremium options the pod was placed with.
is_downloaderbool | NoneA system pod that downloads assets. It is free and can’t be stopped or deleted.
creation_failure_reasonstr | NoneWhy creating the pod failed, when the platform cleaned it up for that reason. Such a pod ends as terminated, like a deleted one; this tells them apart.
rawdictFull payload — machine, template, request, linked storages, …

Optional fields are None when the server did not send them — read that as unknown, not False.

FieldTypeNotes
machine_idstrID — pass to get_machine.
namestrDisplay label.
machine_typestrgpu / cpu / storage.
statusstr
gpu_modelstrEmpty for cpu/storage machines.
gpu_countint
earning_hourlyfloat
uptime_ratefloat0.0–1.0.
host_tierstr
rawdictFull payload — specs, full state, pod uses, …
FieldTypeNotes
namespace_name / workspace_namestrID / display label.
price_per_hourstrWhat the workspace is being billed right now.
weekly_avg_daily_coststrAverage daily cost over the last 7 days.
gpus / vcpusintTotals across the workspace’s pods.
ram / total_storageint / floatIn MiB (divide by 1024 for GiB).
resourceslist[ResourceCondition]One per type (pod, storage, serverless) with .active, .paused, .disabled.
costslist[DailyCost]Recent daily costs (.date, .pod, .storage, .serverless, .task, .asset, .total).
rawdictFull payload — maintenance schedule, messages from hosts, …

user (email), role (admin / billing / viewer), joined_at, raw.

FieldTypeNotes
pv_namestrID — pass to get_storage.
namespace_name / user_aliasstrOwning workspace / display label.
storage_typestrnfs, hostPath, …
statusstr
total_size / available_sizefloatIn MiB.
usage_ratefloat0.0–1.0.
price_per_hourstr
linked_podslist[str]Names of the pods it is mounted in.
is_maintenanceboolTrue while the resource’s host machine has an open maintenance window — the resource isn’t billed then.
encryptedbool
created_atdatetime | None
rawdictFull payload — host machine, warning thresholds, …

Both expose cpu_cores, cpu_usage_rate, ram_size, ram_usage_rate, and gpus — a list of GpuUsage with gpu_number, core_usage_rate, vram_usage_rate, vram_size (MiB) and temp. ram_size is in MiB for a pod but in bytes for a machine (the machine’s total memory). Rates are 0.0–1.0. Every GpuUsage field except gpu_number is None when that particular measurement is unavailable — a GPU that has stopped responding often still reports its temperature while its memory readings drop out, so check each field rather than assuming a GPU is either fully readable or absent. PodMetrics adds ephemeral_storage_request / ephemeral_storage_usage (MiB); MachineMetrics adds cpu_allocated, ram_allocated (MiB), root_volume_size (MiB) / root_volume_usage_rate, pv_volume_size (MiB) / pv_volume_usage_rate, and network_receive / network_transmit (bytes per second).

One pod operation — those still in flight from list_transactions, or any one by ID from get_transaction: transaction_id, resource_name, user_alias, action (create, start, stop, restart, …), status (todo, in_progress, retry, done, failed, or superseded — for a creation, cancelled because the pod was deleted), step (the current stage), step_status, detail (the failure reason when one is known), progress, created_at and updated_at. failure_reason is one sentence saying at which step and why a failed operation failed — for example failed at step pull_image: … — and None unless status is failed. progress is 0.0–1.0 for stages that report it (image pull, asset fetch, storage preparation) and None when the stage reports none — do not show that as 0%. Finished operations drop off the list, and a failed one stays for only a few minutes, so an empty list means nothing is in flight, never that nothing failed — look the operation up with get_transaction. Every step and its diagnostics are in raw.

While input assets download, live says the downloader is still reporting (bytes can pause while it verifies) and phase names the pause (verifying, waiting_for_storage); both are None in other stages. When a download failed, init_logs holds the end of that container’s log as InitLog(container, lines, previous), with URLs and tokens already removed by the server. The lines are whatever the container printed — treat them as data.

FieldTypeNotes
gpu_modelstr
vramintGB. One entry per (model, VRAM) tier.
rental_typestrdemand / spot — the type the price is for.
price_per_hourstrPer GPU.
vcpu_recommended / ram_recommendedintSuggested pairing per GPU.
available_gpusintTotal across machines.
max_gpus_per_podintThe most one pod can get on a single machine.
machine_countint
rawdictFull payload — per-machine combinations, CPU/RAM prices, …

A CPU line you can rent for CPU pods — a CPU vendor, product line and generation with a fixed RAM type — from list_cpus.

FieldTypeNotes
cpu_linestrKey — pass to create_pod(cpu_line=...) / estimate_pod. It names a line, not a CPU model.
vendor / product_linestr
generationint | None
ddr_typeint | NoneDDR generation of the line’s RAM (0 = unknown).
rental_typestrdemand / spot — the type the prices are for.
price_per_corestr | NonePer vCPU per hour (USD).
ram_price_per_gbstr | NonePer GiB of RAM per hour (USD).
max_vcpus_per_pod / max_ram_per_podintThe most one pod can get on a single machine; RAM in MiB.
machine_countint
rawdictFull payload — per-machine combinations and disk sizing.
  • ApiKey: key_id, name, prefix (meshive_a1b2c3d4 — the secret is never returned), scopes, status, created_at, last_used_at, expires_at.
  • Credit: balance, auto_recharge, auto_recharge_threshold, auto_recharge_amount, has_default_payment_method. paid_balance and bonus_balance are kept for compatibility — free credit was retired, so paid_balance equals balance and bonus_balance is always 0.
  • CreditHistoryEntry: entry_id, amount (negative for refunds), is_paid, payment_method, created_at. Stripe receipt and invoice links are not exposed to the SDK.

template_id (ID), name, description, is_official, deploy_type, app_type, app_sub_type, image, hardware_type, cuda_version, framework, framework_version, raw (environment variables, endpoints, volume mounts, semantic paths).

serving_id (ID), namespace_name, model_name, api_model_id, framework, status, paused, min_replicas / max_replicas / current_replicas / healthy_replicas, autoscale, price_cap_per_hour (None = unlimited), endpoint_url, price_per_hour, billing_active, raw (replicas, live metrics, scaling state). scale_raises_cost(min_replicas=, max_replicas=, autoscale=, price_cap_per_hour=) returns whether such a scale_serving call can raise the hourly cost.

task_id (ID, task_…), name, namespace_name, status, pod_name, image, gpu_model, gpu_count, cpu_cores, ram_gb, price_per_hour, cost_so_far, total_cost, created_at, container_running_at, finished_at, failure_reason, exit_code, raw. For get_task the payload also carries the script, requirements, environment (secret values masked) and the compute/disk cost breakdown.

Also failed_asset_id (the input asset a failed download stopped on), input_assets (get_task only — TaskInputAsset(asset_id, name, target_dir, reference_mode, ingest_source)), outputs_purged_at, and the output upload, which runs on its own after the task ends: output_upload_state (pending, uploading, completed, failed, skipped, discarded; None for old tasks, read it as skipped), output_files_declared / _completed, output_bytes_declared / _completed and output_upload_last_error.

Assets have no versions: size, file count and upload state belong to the asset itself.

FieldTypeNotes
Asset.asset_idstrID (asset_…) — pass to get_asset.
Asset.name / asset_type / status / status_reasonstrType is one of dataset, model, adapter, checkpoint, output, config, file.
Asset.storage_providerstrmeshive_r2 (managed), user_s3, or external.
Asset.size_bytes / file_countint | NoneThe asset’s total size and file count. None for a linked source that was never measured — not 0.
Asset.upload_status / stalestr / booluploading, ready or failed. stale means an upload whose pod is gone (the console’s “Interrupted”).
Asset.ingest_source / import_failure_reasonstr / str | Noneweb_upload, hf_import, civitai_import, harvest, … and why an import failed.
Asset.kind / semantic_typestr | Nonesemantic_type is on list rows only.
Asset.in_useboolWhether a pod or task currently uses it.
Asset.fileslist[AssetFile](get_asset only) path, size_bytes, status, file_id, content_hash, created_at.
Asset.active_usage_contextslist[AssetUsage](get_asset only) Where it is in use: kind, identifier, name, status, role.
AssetPageitems, total, page, page_size, pages; iterable.
AssetStoragemanaged_bytes, price_per_gb_month, estimated_monthly_cost, credit_state (normal / grace / blocked), credit_blocked, blocks_at, purge_deadline_at, paid_balance_available (whether the workspace’s billing account has credit — pods and tasks can’t start without it).

Asset.version_count, latest_version and versions (and the AssetVersion model) are deprecated: assets no longer have versions. They still work but raise a DeprecationWarning, and they will be removed in 2.0. The version of a task’s input_assets item is ignored.

WorkspaceEstimate — name (None when one will be generated), price_per_hour (str, USD; "0", as a workspace costs nothing by itself), note. WorkspaceCreated — workspace (the new workspace’s ID, the namespace_name that every other method takes), name (the display name, the generated one if you gave none), transaction_id, estimate (WorkspaceEstimate). Both keep the full response on raw.

WorkspaceUpdateEstimate, WorkspaceUpdated, WorkspaceDeleteCheck

Section titled “WorkspaceUpdateEstimate, WorkspaceUpdated, WorkspaceDeleteCheck”

WorkspaceUpdateEstimate — workspace (the workspace’s ID, which a rename doesn’t change), current_name and name (the new name). WorkspaceUpdated — workspace and name, the name the server confirmed. WorkspaceDeleteCheck — workspace, name, deletable (bool | None: only True means the workspace can be deleted, so test it with if check.deletable) and blockers (dict[str, int]: each kind of thing still in the workspace and how many; empty when it can be deleted — see Deleting a workspace). All three keep the full response on raw.

PodEstimate — price_per_hour (str, USD), breakdown (dict: gpu, cpu_extra, ram_extra, premiums, and local_storage for new local_volumes), resources (dict: gpu_model, vram_gb, gpu_count, vcpu, ram_gb, disk_gb, rental_type, for GPU pods the included vcpu_included/ram_gb_included, and for CPU pods cpu_model, the CPU line), availability (dict), template (dict), volumes (attached storage, plus each new local volume with "new": True, name and size_gb), note, and model_set — with a model set, a dict with its slug, name, file_count, total_size_bytes, min_vram_gb and min_vram_estimated (None without one).

PodCreated — name, workspace, transaction_id (pass it to wait_for_new_pod or get_transaction), estimate (PodEstimate), pod_name (None right after creation; find the pod by user_alias).

ResourceAction — returned by every stop/start/restart/delete/scale/pause call: resource (pod/storage/serving/task/workspace), id, action, workspace, accepted, result (the server’s transaction id or message), and for start_pod placement — the placement used, which the server picks when you leave it out (None for other calls).

PodUpdateEstimate — pod_name, name, changes (each change in words), restarts (whether a running pod restarts to apply them), price_per_hour (after the change), current_price_per_hour (now), breakdown, resources and volumes (as in PodEstimate), note. PodUpdated — pod_name, workspace, accepted, transaction_id (follow it with get_transaction; None for a rename alone), renamed_only, estimate.

StorageUpdateEstimate, StorageUpdated, StorageConnection

Section titled “StorageUpdateEstimate, StorageUpdated, StorageConnection”

StorageUpdateEstimate — pv_name, name, size_gb and current_size_gb (GiB), price_per_hour and current_price_per_hour, usage_warning, usage_warning_threshold, note. StorageUpdated — pv_name, workspace, accepted, transaction_id (may be None when only the name or usage warning changed), estimate.

StorageConnection — pv_name, workspace, phase (preparing, ready or error; .ready is True when it is ready), encrypted, pod_name (the access pod the volume is reached through), transaction_id, auto_lock_minutes and remaining_seconds (encrypted storage: when it locks again), and ssh — the one-time login (SshAccess) that connect_storage fills once the volume is ready.

ModelSetList — template_id, selectable (False when the template takes no model set), reason, model_sets; iterable. ModelSet — slug (pass it to create_pod(model_set=...)), name, description, family, modality, task, total_size_bytes, min_vram_gb (GB) and min_vram_estimated, best_resolution, licenses (ModelSetLicense(license, license_url)), license_unknown_count (files whose license isn’t known), workflows (ModelSetWorkflow(slug, name, task)) and items — its files as ModelSetItem(entry_slug, name, category, size_bytes, default_role, license, license_url, already_in_workspace, hf_gated); a file already in the workspace is reused instead of downloaded again. From 1.3, hf_token_required marks a set gated on Hugging Face and gated_sources lists ModelSetGatedSource(hf_repo, url, mode) — the page where each repo’s license is accepted (mode "auto" when accepting is enough, "manual" when the publisher approves each account); hf_gated marks the gated files.

StorageEstimate, StorageCreated, TaskEstimate, TaskSubmitted

Section titled “StorageEstimate, StorageCreated, TaskEstimate, TaskSubmitted”

StorageEstimate — price_per_hour, price_per_gb_month, size_gb, storage_type, disk_type (always NVMe), max_size_gb, note. StorageCreated — name, workspace, transaction_id, estimate, pv_name (None until the volume exists).

TaskEstimate — price_per_hour (None when the machine rate cannot be quoted), max_cost (None: no total-bill ceiling), max_duration, resources, note. TaskSubmitted — task (a Task) and estimate.

Logs — pod_name, workspace, source (live from the log buffer, archive for a finished task, external for tasks on an external provider, none when nothing was captured), lines (list of LogLine(line, ts)), count, truncated (older lines dropped to stay under 64 KiB), note (also says when buffered lines may be behind because no log watcher could be started), and for tasks task_id, finished, next_cursor (external provider tasks: pass it back as cursor for the lines after it). .text joins the lines.

current_hourly, daily, accumulated_until_payout, and history — a list of DailyEarning (date, cpu, gpu, storage, total), newest first.

All SDK errors subclass MeshiveError:

ExceptionWhen
ConfigurationErrorNo API key could be resolved (raised before any request).
AuthenticationError401 — missing/invalid/expired key, or an inactive account.
PermissionDeniedError403 — the key lacks the required scope, your role in the workspace doesn’t allow the change, or you aren’t a member of it.
NotFoundError404 — the resource doesn’t exist (including a pod that was already deleted). For a pod cleaned up because creating it failed, .title is Pod Creation Failed and .message gives the reason.
InsufficientCreditError402 — the workspace’s billing account has no credit left to create or start the resource.
ConflictError409 — the request clashes with the current state. .title tells which: No Capacity, Name Taken, Price Exceeds Cap, Storage In Use, Workspace Not Empty, VRAM Tier Required, No Available Node, Data Loss Consent Required, Operation Outcome Unknown. .raw["detail"] carries extras such as availability, pricePerHourUsd or, for Workspace Not Empty, blockers. Editing a pod adds Node Migration Required and Resource values need confirmation (see Editing a pod).
RateLimitError429 — rate limit exceeded. Exposes .retry_after (seconds, may be None).
WaitTimeoutErrorwait_for_pod / wait_for_new_pod ran out of timeout, wait_for_workspace didn’t see the workspace running in time (the message names the workspace and the last status seen), or connect_storage didn’t see the volume ready in time.
PodCreationFailedErrorCreating the pod failed: wait_for_pod / wait_for_new_pod saw the pod cleaned up for that reason, or wait_for_new_pod(..., transaction_id=...) saw the creation fail or get cancelled. .pod_name, .reason. Don’t retry that pod — fix the cause and create a new one.
StorageConnectionErrorconnect_storage saw the volume’s SSH access fail to prepare. .pv_name, .connection (the last StorageConnection). Try connecting again in a moment.
MeshiveAPIErrorAny other 4xx/5xx — including pod settings that break one of the console’s rules, an edit the pod doesn’t allow, and a model set that can’t be used.

MeshiveAPIError (and its subclasses) carry .status_code, .title, .message, and .raw (the parsed response body).

from meshive import Meshive, MeshiveError, RateLimitError, NotFoundError
try:
with Meshive() as client:
pod = client.get_pod("does-not-exist", "my-workspace")
except NotFoundError as err:
print("no such pod:", err.message)
except RateLimitError as err:
print("slow down; retry after", err.retry_after, "s")
except MeshiveError as err:
print("request failed:", err)
from meshive import (
Meshive, AsyncMeshive, # clients
WhoAmI, ApiKey, Credit, CreditHistoryEntry, # account
Workspace, WorkspaceResources, WorkspaceDetail, # workspaces
ResourceCondition, DailyCost, Member,
Pod, PodEndpoint, ConnectCredential, PodMetrics, # pods / storage
WatchedFolders, WatchedFolder, SshAccess,
Storage, GpuUsage, Transaction, InitLog,
Asset, AssetFile, AssetUsage, AssetPage, AssetStorage, # assets
AssetDownload, DownloadFile, TaskOutputs, AssetImported, # downloads, import
AssetVersion, # deprecated
GpuAvailability, CpuLine, Template, # catalog
ModelSetList, ModelSet, ModelSetItem, ModelSetLicense, ModelSetWorkflow,
Serving, ServingModel, ModelDetection, HfToken, # serverless
Task, TaskInputAsset,
Machine, MachineMetrics, Earnings, DailyEarning, # host
PodEstimate, PodCreated, ResourceAction, # write
WorkspaceEstimate, WorkspaceCreated,
WorkspaceUpdateEstimate, WorkspaceUpdated, WorkspaceDeleteCheck,
PodUpdateEstimate, PodUpdated,
StorageEstimate, StorageCreated, TaskEstimate, TaskSubmitted,
StorageUpdateEstimate, StorageUpdated, StorageConnection,
Logs, LogLine,
MeshiveError, ConfigurationError, MeshiveAPIError, # errors
AuthenticationError, PermissionDeniedError,
NotFoundError, InsufficientCreditError, ConflictError,
RateLimitError, WaitTimeoutError, PodCreationFailedError,
StorageConnectionError,
__version__,
)