/v1/deployments/*. All endpoints require Bearer token auth (Authorization: Bearer vk_...).
Endpoints
POST /v1/deployments
Create a deployment from a base HF model, a completed training job, or a saved model in your library. Request schema:
Validation: When
source=training_job, the job must exist in your workspace (404 otherwise) and be completed (400 otherwise); a custom_script job is a 400 too, since its output is whatever the script wrote rather than a checkpoint Veri can serve (push the weights to a Hugging Face repo from your script and deploy that repo with source=huggingface). When source=custom_model, the saved model must exist in your workspace (404) and be ready (409 while it is still importing). A job trained with lora_rank (or a saved model made from one with the default full artifact) is served from the merged weights Veri keeps next to the adapter. A LoRA job trained before merged weights were kept has none, and is a 400 in both cases: serve it as merged weights from Hugging Face (hf_push with "artifact": "merged", then deploy that repo) or attach it to a serving_mode: "multi_adapter" deployment of its base model (multi-adapter fleets, beta). A model saved with artifact: "adapter" is also a 400 on this path (fleets only). An unsupported GPU shape is a 400. Other errors: 402 when the workspace has no billing account yet, and 429 with code ACTIVE_DEPLOYMENT_LIMIT when the new replicas don’t fit the workspace’s active-replica limit (2 by default: every replica of a queued, provisioning, serving, or waking deployment counts).
A field that fails validation, on create or on PATCH, returns 400 with a stable code next to the message, so you can branch on it instead of matching the text:
Other
400s (an unknown source, a training job that is not completed, a LoRA checkpoint without merged weights, a provider that is not enabled, a deployment that is stopped) carry only a message. The Python SDK puts the code on VeriError.code.
Response (201):
endpoint_url populates when status reaches serving: it is https://api.veri.studio/v1/deployments/{id}/chat/completions (call it with your Veri API key). It reads null while the deployment is parked (scaled_to_zero) and is set again once a wake serves; the platform URL itself never changes, and a request to it is what wakes a parked deployment.
When a deployment fails, error holds the human-readable message and failure_code a stable, machine-readable reason you can branch on, for example watchdog_timeout, CAPACITY_UNAVAILABLE, vllm_failed_to_start, oom, model_source_unsupported, or replica_lost. Both are null while nothing has failed. An idle park and a wake clear both; a wake that fails re-parks the deployment with the wake’s error and code (for example wake_boot_failed or CAPACITY_UNAVAILABLE).
replica_lost means the deployment lost its last GPU box after it had served. While status is provisioning, Veri is replacing the box (Self-healing) and the code clears once a replacement serves; with failed (or scaled_to_zero for a min_replicas: 0 deployment) the self_heal_attempts budget was used up or set to 0.
Three fields report the wake state of a scale-to-zero deployment:
Extra vLLM flags
vllm_extra_args (vllm engine only) appends tokens to the engine command, for example ["--max-model-len", "8192"] or ["--enable-auto-tool-choice", "--tool-call-parser", "hermes"]. Only these flags are accepted. _ may stand for -, and a value goes either as the next token or after =.
Any other flag returns
400 with code invalid_vllm_extra_args and the flag’s name. That includes abbreviations (--max-model), dotted JSON keys (--speculative-config.method ngram: pass the whole JSON value instead), flags Veri manages (--model, --port, --host, --api-key, --served-model-name), and flags that load code, read files on the box, or open ports (for example --trust-remote-code, --chat-template, --tool-parser-plugin, --middleware, --allowed-local-media-path, --grpc). The stored list is checked again each time a replica boots, so a deployment created with a flag outside this list starts without its extra args.
Multi-adapter fleets (beta)
A deployment created with"serving_mode": "multi_adapter" serves its base model plus LoRA adapters attached under aliases: a chat request whose model is an alias is answered by that adapter, and one deployment can serve many adapters of the same base. Optional create fields max_loras (default 32), max_lora_rank (default 64) and max_cpu_loras (default 128) size the fleet. Attach an adapter with POST /v1/deployments/{id}/adapters and a body of {"serve_name": "<alias>", "model_id": "<library model id>"}.
A request for an adapter is only sent to a replica that reports that adapter version loaded. While no replica has it loaded yet (it is still attaching, a flip or rollback is loading the new version, or a replacement replica is still loading it), the request gets 503 with Retry-After: 5 and code upstream_unavailable; retry after the header. Requests to the base model are not affected.
Multi-adapter fleets are in beta and API-only. Known limitations:
- Create the fleet through the REST API: the SDK’s
client.deployments.create()has noserving_modeargument, and the CLI has no adapter commands. The SDK does wrap the adapter endpoints:client.deployments.attach_adapter,flip_adapter,rollback_adapteranddetach_adapter. - Only a library model saved as an adapter can be attached. Save the LoRA training job first (
veri models save <job_id> --name <name> --artifact adapter, orclient.models.create_from_training_job(job_id, name=..., artifact="adapter")); attaching a training job id returns404. - An adapter’s LoRA rank must be at most the fleet’s
max_lora_rank. This is not checked at attach: an adapter with a higher rank never finishes attaching, so requests for it keep getting503. - On a fleet that is
scaled_to_zero, a flip returns409, and aproductionpromotion or rollback reaches the fleet only after it wakes. - The chat response’s
modelfield shows the adapter’s internal versioned name, not the alias you sent.
GET /v1/deployments
List your deployments. Filter bystatus (queued / provisioning / serving / scaled_to_zero / waking / stopped / failed).
PaginatedList[DeploymentResponse].
GET /v1/deployments/{id}
Single deployment. Same shape as create response, with timestamps populated as it runs.PATCH /v1/deployments/{id}
Partial update of the scaling settings on a live deployment. Mutable fields:num_replicas, min_replicas, max_replicas, concurrency_target, scale_to_zero_window_seconds, idle_delete_after_days, self_heal_attempts (0 to 5). Anything else (model, gpu, source, engine, provider) is immutable and fails validation. Any PATCH also restarts the replacement budget of self-healing.
Returns the updated deployment immediately with the new desired state; the control plane converges the replica fleet in the background. Compare ready_replicas (observed) against num_replicas (desired) to watch convergence.
A field you leave out keeps its stored value. scale_to_zero_window_seconds and idle_delete_after_days also take an explicit null, which clears them back to their defaults (the 3600-second idle window; never auto-stop a parked deployment). On any other field, null is the same as leaving it out.
min_replicas to 0), or changing or clearing scale_to_zero_window_seconds, starts a fresh idle window, so a deployment that has already been idle does not park right after the update.
When the update takes effect depends on the deployment’s status:
self_heal_attempts is the exception: in every status it applies immediately, as the policy for the next time the deployment’s last box dies while serving. Any PATCH also restarts the replacement count, so a self-heal in progress gets the full new budget.
Waking on update is checked against the workspace active-replica limit before anything is saved: if the new floor does not fit, the update returns 429 with code ACTIVE_DEPLOYMENT_LIMIT and the deployment stays parked with its previous settings. An update that raises num_replicas on a running deployment is checked the same way.
GET /v1/deployments/{id}/replicas
List the replicas (GPU boxes) behind a deployment: per-replica status, placement, and the latest engine snapshot (in-flight requests, KV-cache usage). Use it to pick areplica_id for the drain endpoint below.
POST /v1/deployments/{id}/replicas/{replica_id}/drain
Retire one specific replica of aserving deployment. A replica taking traffic finishes its in-flight requests before its box is terminated (at least 60 seconds after its drain starts, at most 10 minutes). A replica still booting (queued or provisioning) has no traffic to protect and is cancelled outright in either mode.
Two modes:
GET /v1/deployments/{id}/replicas the old replica shows replace_requested_at, and the replacement shows replaces_replica_id (the old replica’s id) while it boots. When the replacement serves, the old replica becomes draining and both fields clear. Both boxes are billed during the overlap: the replacement’s boot (typically 5 to 12 minutes) plus the old replica’s drain. While a replace is in progress the fleet does not scale down; scale-up still happens.
If the replacement fails (it crashes, it does not become ready within its provisioning deadline, or no capacity is available to launch it), the old replica keeps serving, the replace is cleared, and the reason is stored in replace_error on the old replica. It is not retried automatically: call the drain with replace=true again. A failed replacement does not count toward the deployment’s self-heal (replacement) attempts, and it is billed only for the time it ran.
Errors:
Drains of the same deployment are applied one at a time, so two concurrent default-mode drains cannot together remove the last replica serving traffic or go below the floor.
While autoscaling is armed (
min_replicas < max_replicas) the autoscaler may scale back up after a default-mode drain if load warrants it; that is expected. Lower max_replicas to hold a smaller fleet.
POST /v1/deployments/{id}/wake
Pre-warm ascaled_to_zero deployment. Returns 202 with the deployment; idempotent (a no-op if it’s already up or booting). A chat request to a parked deployment also wakes it: raw HTTP clients receive 503 with a Retry-After header and error code deployment_waking while the GPU boots, while the Python SDK’s chat() retries through the wake automatically (see the chat endpoint). Fire wake early on a predictive signal to hide the cold start entirely. Like a create, a wake (explicit or request-triggered) is checked against the workspace active-replica limit: with no headroom it returns 429 with code ACTIVE_DEPLOYMENT_LIMIT and the deployment stays scaled_to_zero until you free capacity.
POST /v1/deployments/{id}/stop
Transition tostopped. This is permanent, not a pause: a stopped deployment cannot be woken or resumed (use min_replicas: 0 to park on idle instead). A final billing tick settles the last partial minute of each replica. Returns the updated deployment. Idempotent: calling it on an already stopped or failed deployment returns the row unchanged.
POST /v1/deployments/{id}/chat/completions
OpenAI-compatible. See OpenAI compatibility for the supported subset of fields. Request:Waking from zero
A chat request to ascaled_to_zero deployment triggers its wake and answers 503 with a Retry-After header while the GPU boots:
Retry-After seconds. The Python SDK retries for you: chat() sleeps and re-sends until the deployment serves the request or wake_timeout_s (default 1320, 22 minutes) elapses, calling on_waking(message) on each 503 so you can surface progress. Set wake_timeout_s=0 to fail fast with the 503 instead. The server’s boot deadline is longer than the SDK default: 40 minutes plus 5 seconds per GB of the deployment’s total GPU memory (about 42 minutes on L4-24GB x1), so raise wake_timeout_s if your cold starts take longer than 22 minutes.
503 deployment_waking answers every request while the deployment boots without a serving replica: a first boot (queued, provisioning), a wake (waking, then provisioning), and a self-healing replacement of a lost last box (provisioning with failure_code: replica_lost). The Retry-After is 30 seconds.
If a wake fails (no GPU capacity, or the box fails or does not become ready within the boot deadline), the deployment parks again (scaled_to_zero, still wakeable), error holds the cause, wake_failure_count goes up by one and next_wake_not_before is set. Until that time a chat request does not start another wake: it gets 503 deployment_waking with Retry-After set to the seconds left and the cause in the message. The wait starts at 60 seconds and doubles with each failed wake in a row, up to 1 hour. POST /wake, or a PATCH that raises min_replicas to 1 or more, retries at once. With min_replicas of 1 or more you don’t need traffic to recover: Veri starts the next wake itself once next_wake_not_before has passed, so the replica floor comes back on its own (if the workspace active-replica limit is full at that point, the deployment stays parked and Veri retries as soon as there is room). Veri stops retrying on its own after 3 failed wakes in a row, so a deployment that cannot boot does not keep launching GPU boxes: it stays scaled_to_zero, and error says Veri stopped retrying automatically. Call POST /wake or PATCH min_replicas to try again (a chat request after next_wake_not_before also wakes it). If that wake fails too, the deployment stays parked with no automatic retry until a wake reaches serving. Both fields reset when a wake reaches serving, and Veri’s own retries resume.
Upstream unavailable
The same503 + Retry-After shape covers transient upstream failures: the model server is unreachable, answers 5xx or 429, no replica is serving, or (on a multi-adapter fleet) no serving replica has the requested adapter loaded yet. The error code is upstream_unavailable:
Retry-After seconds, as for deployment_waking. Both codes mean “try again shortly”, not “the request was invalid”. The Python SDK’s chat() retries upstream_unavailable for you too, but with a small fixed budget (upstream_retries, default 3, each after Retry-After) since a replica blip resolves in seconds while a wake takes minutes; after the budget the 503 surfaces as VeriServerError with code == "upstream_unavailable".
POST /v1/deployments/{id}/responses
The OpenAI Responses API, forwarded to the deployment’s vLLM/v1/responses (the Codex CLI’s only model API). Same authentication, wake (503 deployment_waking) and upstream_unavailable contract as chat completions; model is rewritten to the deployment’s served name and stream: true returns the Responses event stream. Also served at /v1/deployments/{id}/v1/responses. See Responses API.
GET /v1/deployments/{id}/requests
Per-request log: prompt tokens, completion tokens, latency, status code, error (if any).status_code >= 400) for debugging.
model_version_id pins the request to the registered model version that
answered it: on a multi-adapter fleet (beta), the version behind the adapter alias the
request resolved through (an alias is a moving pointer, so after a production
flip the same alias serves different weights); for base-model requests on a
fleet built from a registered model, that model’s version. null when nothing
served belongs to a model lineage.
GET /v1/deployments/{id}/metrics
Lifetime counters, plus latency and error stats for the past 24 hours:total_cost_usd is the GPU cost accrued over the deployment’s life: every replica of every boot, from its launch to its termination (or now), at its per-box rate, before rounding to the cent. uptime_seconds is the time at least one box was up, summed over every boot; time spent parked is not counted. See Billing.
State machine
The lifecycle diagram shows every transition. In short:
Billing is per replica: each GPU box bills from its launch until it is terminated, so boot (
provisioning) and drain time are billed. Boot time is charged once the deployment serves, parks, stops, or fails. queued, waking, and scaled_to_zero cost nothing (a wake’s boxes bill from their own launch). A stopped or failed deployment accrues nothing further: each box that ran is settled up to the moment it was freed. See Billing.
Internal callbacks
The GPU boxes behind a deployment report their status and heartbeats to the control plane atPOST /v1/deployments/{id}/status, authenticated with a per-deployment token that only the boxes hold. This route is internal to the platform: it is not part of the customer API, and your API key cannot call it.
SDK convenience
The SDK wraps the create flow into aDeployment dataclass with bound methods:
wait() returns once the status settles: serving, failed, stopped, or scaled_to_zero (a parked deployment won’t move again without traffic or a wake retry, so waiting on it would just burn the timeout). dep.chat() retries through a wake with the default 22-minute budget and accepts upstream_retries and timeout_s; call client.deployments.chat() directly to tune wake_timeout_s or on_waking.
Related
OpenAI compatibility
What chat fields are supported.
Billing
Billing states and idle warning.
Hosting & GPU sizing
Pick the right GPU.
Deployment observability
Request, token, latency, and replica metrics.

