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).
Quick start
Section titled “Quick start”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)import asynciofrom meshive import AsyncMeshive
async def main(): async with AsyncMeshive() as client: me = await client.me() print(me.email, me.user_role)
pods = await client.list_pods("my-workspace") pod = await client.get_pod(pods[0].pod_name, "my-workspace") print(pod.status, pod.raw)
gpus = await client.list_gpus(min_vram=40) tasks = await client.list_tasks("my-workspace", status="running")
machines = await client.list_machines() machine = await client.get_machine(machines[0].machine_id) print(machine.earning_hourly)
asyncio.run(main())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.
Constructing a client
Section titled “Constructing a client”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.
Many clients in one program
Section titled “Many clients in one program”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 httpxfrom 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 shutdownawait 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 awithblock 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_PROXYand 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.
Methods
Section titled “Methods”Both clients expose the same methods (await them on AsyncMeshive):
| Method | Returns | Description |
|---|---|---|
me() | WhoAmI | The current API key’s owner. |
list_api_keys() | list[ApiKey] | Your active API keys (prefixes only). |
get_credit() | Credit | Credit 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) | WorkspaceDetail | Cost 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) | Workspace | Poll 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) | Pod | A single pod. |
wait_for_pod(pod_name, workspace, *, until="running", timeout=600, interval=5) | Pod | Poll 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) | Pod | Wait 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) | PodMetrics | Live 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) | Transaction | One 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) | Storage | A 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) | Template | A single template (custom ones need their workspace). |
list_model_sets(workspace, template_id) | ModelSetList | The 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) | Serving | A 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) | Task | A single task. |
list_assets(workspace, *, asset_type=None, status=None, page=1, page_size=20) | AssetPage | One page of a workspace’s assets. Iterate it, or read .items, .total, .pages. |
get_asset(asset_id) | Asset | A single asset with its files and where it is in use. |
get_asset_storage(workspace) | AssetStorage | Managed 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) | AssetImported | Link 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) | AssetDownload | Short-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) | Machine | A single machine. |
get_machine_metrics(machine_id) | MachineMetrics | Live metrics of a machine. |
get_earnings(*, start_date=None, end_date=None) | Earnings | Host earnings summary and daily history. |
get_pod_logs(pod_name, workspace, *, tail=200, container=None, wait=None) | Logs | Last tail lines of a pod’s logs (see Logs). |
get_task_logs(task_id, *, tail=200, wait=None, cursor=None) | Logs | Last 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) | dict | Read 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. |
Write methods
Section titled “Write methods”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.
| Method | Returns | Description |
|---|---|---|
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) | WorkspaceCreated | Create 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) | WorkspaceUpdated | Rename 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) | ResourceAction | Delete 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, ...) | PodEstimate | Hourly 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) | PodCreated | Create 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) | PodUpdated | Change 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) | SshAccess | A 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) | WatchedFolders | A 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) | WatchedFolders | Replace 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) | ResourceAction | Scale the pod to zero (pod billing stops, storage billing continues). |
start_pod(pod_name, workspace, *, placement=None, allow_data_loss=False) | ResourceAction | Start 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) | ResourceAction | Restart in place. |
delete_pod(pod_name, workspace, *, delete_local_storages=None) | ResourceAction | Delete 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) | StorageEstimate | Hourly price of a network volume. |
create_storage(name, size_gb, *, workspace, ...) | StorageCreated | Create 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) | ResourceAction | Delete 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) | StorageUpdated | Rename 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) | StorageConnection | Open 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) | StorageConnection | Where 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) | ResourceAction | Register 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) | ResourceAction | Remove 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) | ResourceAction | Deploy a registered model as a serving. |
scale_serving(serving_id, *, min_replicas=None, max_replicas=None, autoscale=None, price_cap_per_hour=None) | ResourceAction | Change 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) | ResourceAction | Pause (True) or resume (False). |
delete_serving(serving_id) | ResourceAction | Delete 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) | TaskEstimate | Compute 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, ...) | TaskSubmitted | Submit the task (same arguments as the estimate). Use print(..., flush=True) in the script. |
stop_task(task_id) | ResourceAction | Stop 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.
| Setting | Rule |
|---|---|
vcpu, ram_gb | Must 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_line | CPU pods only. A cpu_line from list_cpus(); without it, the cheapest line that fits is used. |
volumes | Existing network (nfs) storage only. Local storage that already exists can’t be attached to a new pod. |
local_volumes | New 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. |
ports | New 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_ports | Changes 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). |
env | Environment variables the template requires need a value. |
command | At 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"))Price caps and data loss consent
Section titled “Price caps and data loss consent”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.
Starting a stopped pod
Section titled “Starting a stopped pod”Without a placement, start_pod lets the server pick what the console’s Start button would, from the pod’s state:
| The pod | Placement 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 else | same_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.
Editing a pod
Section titled “Editing a pod”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 change | Pass |
|---|---|
| Name | name. A rename alone applies in place without a restart (renamed_only is True). |
| Template | template_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. |
| Hardware | gpu_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. |
| Environment | env sets or adds variables (secret_keys marks secret ones); remove_env removes variables you added. |
| Ports | ports opens new ports, template_ports renames, renumbers, exposes or hides current ones (as in create_pod), and remove_ports closes ports you added. |
| Command | command replaces the start command; "" goes back to the template’s. |
| Storage | volumes 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:
| Refusal | Meaning and next step |
|---|---|
ConflictError Node Migration Required | The 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 Required | The move would permanently delete the pod’s unpreserved workspace files (pod.has_unpreserved_workspace). After separate consent, add allow_data_loss=True. |
Cannot Change Resources | The 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 confirmation | An 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)Editing and connecting to storage
Section titled “Editing and connecting to storage”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 itStarting a pod with a model set
Section titled “Starting a pod with a model set”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_podwithmodel_settherefore 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: checklist_pods, orget_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 withConflictErrorOperation Outcome Unknown. - All or nothing. If any file can’t be added, no pod is created:
MeshiveAPIErrorModel Set Unavailablelists the failed files inraw["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 (
selectableisFalse, with areason). A slug the template doesn’t offer is refused withModel Set Not For Template, andraw["detail"]["available"]lists the sets that fit. - Gated sets (1.3+). A set with
hf_token_requiredcomes 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 ingated_sourceswith your Hugging Face account, then create the pod from the set’s Quick Deploy template withhf_token_id— an id fromlist_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) andHugging Face token rejected. A gated set passed asmodel_seton another template is refused withModel Set Needs a Token. See Gated model sets.
Recovering an uncertain write
Section titled “Recovering an uncertain write”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)| State | Meaning and next action |
|---|---|
done | The API response was saved. Repeating the identical request with the same key replays it; check the task/pod/serving/storage separately for completion. |
pending / unknown | The request is fenced against duplicate execution. Reconcile the recorded task/transaction; do not submit a new key or assume waiting will unlock it. |
not_found | No 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.
Creating a workspace
Section titled “Creating a workspace”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.
| Refusal | Meaning and next step |
|---|---|
ConflictError Name Taken | One 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 Available | No 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_nameRenaming a workspace
Section titled “Renaming a workspace”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.
| Refusal | Meaning and next step |
|---|---|
ConflictError Name Taken | Another of your workspaces already has that name. Choose another. |
MeshiveAPIError (HTTP 422) | The name is longer than 63 characters or has control characters. |
PermissionDeniedError | The 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 Found | The 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 nameDeleting a workspace
Section titled “Deleting a workspace”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 key | What counts | How to clear it |
|---|---|---|
pods | Pods 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. |
storages | Storage 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). |
servings | Serving deployments, paused ones included. | Delete them (delete_serving). |
tasks | Tasks 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_outputs | Tasks, 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. |
assets | Assets 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. |
templates | Your workspace’s custom templates (not the official ones). | Delete them in the console. |
models | Models registered for serving (not the official ones). | delete_model, or the console. |
credentials | Saved Hugging Face tokens, CivitAI keys, object-storage credentials and private-registry logins. | Remove them in the workspace’s settings in the console. |
webhooks | Webhook endpoints. | Remove them in the console. |
inference_keys | The workspace’s active keys for the serverless inference API (mk-…). | Remove them in the workspace’s settings in the console. |
members | Members 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. |
invitations | Invitations 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_payouts | Earnings 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_progress | Pod, 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).
| Refusal | Meaning and next step |
|---|---|
ConflictError Workspace Not Empty | Something 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 workspace | A protected workspace, or something was added after the check. check_workspace_delete shows what is left. |
PermissionDeniedError | The 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 Found | The 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}")Creating a pod, end to end
Section titled “Creating a pod, end to end”import uuidfrom 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)Models
Section titled “Models”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.
WhoAmI
Section titled “WhoAmI”| Field | Type | Notes |
|---|---|---|
email | str | |
username | str | None | |
user_role | str | |
raw | dict | Full payload. |
Workspace
Section titled “Workspace”| Field | Type | Notes |
|---|---|---|
namespace_name | str | ID — pass to list_pods / get_pod. |
workspace_name | str | Display label. |
description | str | |
member_count | int | |
status | str | running 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_hour | str | |
resources | WorkspaceResources | .pod, .storage, .serverless counts. Includes stopped pods and paused deployments. |
member_role | str | None | Your role here: admin, billing or viewer. A viewer key gets 403 on every change. |
created_at / updated_at | datetime | None | |
raw | dict | Full 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.
| Field | Type | Notes |
|---|---|---|
pod_name | str | ID — pass to get_pod. |
namespace_name | str | Owning workspace ID. |
user_alias | str | Display label. |
status | str | |
rental_type | str | spot / demand. |
price_per_hour | str | |
storage_rate_per_hour | str | Storage rate, separate from compute. |
has_unpreserved_workspace | bool | None | Whether a node move may lose workspace files. None means unknown; do not assume preservation. |
is_maintenance | bool | True while the resource’s host machine has an open maintenance window — the resource isn’t billed then. |
created_at | datetime | None | |
endpoints | list[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_credentials | list[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_active | bool | None | Whether 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_at | Why a stopped pod stopped. None or user is a normal stop; anything else means the system stopped it. | |
same_node_unavailable_reason | str | None | Why 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_node | bool | None | Computed for stopped pods. |
waiting_mode | str | None | same_node or any_node while the pod is waiting. |
container_running_at | datetime | None | When the container started running. |
cpu_premium / uptime_premium / internet_premium | bool | None | Premium options the pod was placed with. |
is_downloader | bool | None | A system pod that downloads assets. It is free and can’t be stopped or deleted. |
creation_failure_reason | str | None | Why 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. |
raw | dict | Full payload — machine, template, request, linked storages, … |
Optional fields are None when the server did not send them — read that as unknown, not False.
Machine
Section titled “Machine”| Field | Type | Notes |
|---|---|---|
machine_id | str | ID — pass to get_machine. |
name | str | Display label. |
machine_type | str | gpu / cpu / storage. |
status | str | |
gpu_model | str | Empty for cpu/storage machines. |
gpu_count | int | |
earning_hourly | float | |
uptime_rate | float | 0.0–1.0. |
host_tier | str | |
raw | dict | Full payload — specs, full state, pod uses, … |
WorkspaceDetail
Section titled “WorkspaceDetail”| Field | Type | Notes |
|---|---|---|
namespace_name / workspace_name | str | ID / display label. |
price_per_hour | str | What the workspace is being billed right now. |
weekly_avg_daily_cost | str | Average daily cost over the last 7 days. |
gpus / vcpus | int | Totals across the workspace’s pods. |
ram / total_storage | int / float | In MiB (divide by 1024 for GiB). |
resources | list[ResourceCondition] | One per type (pod, storage, serverless) with .active, .paused, .disabled. |
costs | list[DailyCost] | Recent daily costs (.date, .pod, .storage, .serverless, .task, .asset, .total). |
raw | dict | Full payload — maintenance schedule, messages from hosts, … |
Member
Section titled “Member”user (email), role (admin / billing / viewer), joined_at, raw.
Storage
Section titled “Storage”| Field | Type | Notes |
|---|---|---|
pv_name | str | ID — pass to get_storage. |
namespace_name / user_alias | str | Owning workspace / display label. |
storage_type | str | nfs, hostPath, … |
status | str | |
total_size / available_size | float | In MiB. |
usage_rate | float | 0.0–1.0. |
price_per_hour | str | |
linked_pods | list[str] | Names of the pods it is mounted in. |
is_maintenance | bool | True while the resource’s host machine has an open maintenance window — the resource isn’t billed then. |
encrypted | bool | |
created_at | datetime | None | |
raw | dict | Full payload — host machine, warning thresholds, … |
PodMetrics and MachineMetrics
Section titled “PodMetrics and MachineMetrics”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).
Transaction
Section titled “Transaction”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.
GpuAvailability
Section titled “GpuAvailability”| Field | Type | Notes |
|---|---|---|
gpu_model | str | |
vram | int | GB. One entry per (model, VRAM) tier. |
rental_type | str | demand / spot — the type the price is for. |
price_per_hour | str | Per GPU. |
vcpu_recommended / ram_recommended | int | Suggested pairing per GPU. |
available_gpus | int | Total across machines. |
max_gpus_per_pod | int | The most one pod can get on a single machine. |
machine_count | int | |
raw | dict | Full payload — per-machine combinations, CPU/RAM prices, … |
CpuLine
Section titled “CpuLine”A CPU line you can rent for CPU pods — a CPU vendor, product line and generation with a fixed RAM type — from list_cpus.
| Field | Type | Notes |
|---|---|---|
cpu_line | str | Key — pass to create_pod(cpu_line=...) / estimate_pod. It names a line, not a CPU model. |
vendor / product_line | str | |
generation | int | None | |
ddr_type | int | None | DDR generation of the line’s RAM (0 = unknown). |
rental_type | str | demand / spot — the type the prices are for. |
price_per_core | str | None | Per vCPU per hour (USD). |
ram_price_per_gb | str | None | Per GiB of RAM per hour (USD). |
max_vcpus_per_pod / max_ram_per_pod | int | The most one pod can get on a single machine; RAM in MiB. |
machine_count | int | |
raw | dict | Full payload — per-machine combinations and disk sizing. |
ApiKey, Credit, CreditHistoryEntry
Section titled “ApiKey, Credit, CreditHistoryEntry”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_balanceandbonus_balanceare kept for compatibility — free credit was retired, sopaid_balanceequalsbalanceandbonus_balanceis 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
Section titled “Template”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
Section titled “Serving”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.
Asset, AssetPage, AssetStorage
Section titled “Asset, AssetPage, AssetStorage”Assets have no versions: size, file count and upload state belong to the asset itself.
| Field | Type | Notes |
|---|---|---|
Asset.asset_id | str | ID (asset_…) — pass to get_asset. |
Asset.name / asset_type / status / status_reason | str | Type is one of dataset, model, adapter, checkpoint, output, config, file. |
Asset.storage_provider | str | meshive_r2 (managed), user_s3, or external. |
Asset.size_bytes / file_count | int | None | The asset’s total size and file count. None for a linked source that was never measured — not 0. |
Asset.upload_status / stale | str / bool | uploading, ready or failed. stale means an upload whose pod is gone (the console’s “Interrupted”). |
Asset.ingest_source / import_failure_reason | str / str | None | web_upload, hf_import, civitai_import, harvest, … and why an import failed. |
Asset.kind / semantic_type | str | None | semantic_type is on list rows only. |
Asset.in_use | bool | Whether a pod or task currently uses it. |
Asset.files | list[AssetFile] | (get_asset only) path, size_bytes, status, file_id, content_hash, created_at. |
Asset.active_usage_contexts | list[AssetUsage] | (get_asset only) Where it is in use: kind, identifier, name, status, role. |
AssetPage | items, total, page, page_size, pages; iterable. | |
AssetStorage | managed_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, WorkspaceCreated
Section titled “WorkspaceEstimate, WorkspaceCreated”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, PodCreated, ResourceAction
Section titled “PodEstimate, PodCreated, ResourceAction”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, PodUpdated
Section titled “PodUpdateEstimate, PodUpdated”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, ModelSet
Section titled “ModelSetList, ModelSet”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.
Earnings
Section titled “Earnings”current_hourly, daily, accumulated_until_payout, and history — a list of DailyEarning (date, cpu, gpu, storage, total), newest first.
Errors
Section titled “Errors”All SDK errors subclass MeshiveError:
| Exception | When |
|---|---|
ConfigurationError | No API key could be resolved (raised before any request). |
AuthenticationError | 401 — missing/invalid/expired key, or an inactive account. |
PermissionDeniedError | 403 — the key lacks the required scope, your role in the workspace doesn’t allow the change, or you aren’t a member of it. |
NotFoundError | 404 — 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. |
InsufficientCreditError | 402 — the workspace’s billing account has no credit left to create or start the resource. |
ConflictError | 409 — 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). |
RateLimitError | 429 — rate limit exceeded. Exposes .retry_after (seconds, may be None). |
WaitTimeoutError | wait_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. |
PodCreationFailedError | Creating 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. |
StorageConnectionError | connect_storage saw the volume’s SSH access fail to prepare. .pv_name, .connection (the last StorageConnection). Try connecting again in a moment. |
MeshiveAPIError | Any 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)Importable names
Section titled “Importable names”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__,)