Nestor Serve Job API¶
Base URL: https://api.serve.nestor.software
Auth: Authorization: Bearer nsk_... (keep this server-side)
Your key belongs to your account and reaches every endpoint you own. The
endpoint ID in the path selects which. GET /v1/me lists them.
Your endpoint may use fixed, warm-elastic, or scale-to-zero GPU capacity. Jobs are queued and dispatched to a ready worker. Capacity policy determines whether a cold start is possible.
Quickstart¶
export NESTOR_API_KEY=nsk_...
export NESTOR_ENDPOINT=ep_yourname
# 1. submit
curl -sS -X POST https://api.serve.nestor.software/v1/endpoints/$NESTOR_ENDPOINT/jobs \
-H "Authorization: Bearer $NESTOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":{"value":21}}'
# -> 202 {"id":"job_abc...","status":"queued"}
# 2. poll (or skip this entirely and use a webhook)
curl -sS https://api.serve.nestor.software/v1/jobs/job_abc... \
-H "Authorization: Bearer $NESTOR_API_KEY"
# -> {"status":"succeeded","output":{...}}
New here? Start with Getting started and Deployments and runtimes.
A web console is available at
https://api.serve.nestor.software/dashboard, paste the same API key to see live queue depth, worker health, and every job with
its timings, artifacts and errors. Useful while integrating; it is read-only.
Submit a job¶
POST /v1/endpoints/{endpoint_id}/jobs
Content-Type: application/json
Idempotency-Key: your-unique-id (optional but recommended)
{
"input": {
"value": 21
},
"metadata": { "customer_job_id": "84729" },
"webhook_url": "https://your.app/nestor/webhook"
}
Returns 202 Accepted:
{ "id": "job_...", "endpoint_id": "ep_...", "status": "queued",
"created_at": "2026-08-10T14:01:22Z" }
Notes:
- input is runtime-defined JSON. generic_http forwards it as the request
body. comfyui expects an API-format workflow object.
- metadata is stored verbatim and echoed back on every read and webhook. Use it
to carry your own IDs, so you never have to keep a job-ID mapping.
- For ComfyUI endpoints, negative seed and noise_seed values are resolved
deterministically by the runtime driver before execution, so a retried job
renders with the identical seed. Submitted input remains stored unchanged.
- Re-sending with the same Idempotency-Key returns the original job
(HTTP 200, not 202) instead of creating a duplicate. Scoped per endpoint.
Get status / result¶
{
"id": "job_...",
"status": "succeeded",
"attempt": 1,
"output": {
"artifacts": [
{ "type": "image", "url": "https://...", "content_type": "image/png",
"file_name": "ComfyUI_00001_.png", "file_size": 1824075,
"width": 1024, "height": 1024 }
]
},
"timing": { "queue_ms": 121, "execution_ms": 8412 },
"image_digest": "sha256:f357e3e085643f47..."
}
image_digest is the container image that produced this job. During a fleet
rollout nodes update one at a time, so this is how you tell which jobs ran which
bytes. GET /v1/endpoints/{id}/workers shows what each node is currently running.
Statuses: queued → running → succeeded | failed | canceled.
Artifact-producing runtimes may return artifacts. artifacts[].type is
image, video, audio or file, derived from the content type.
Artifact URLs are time-limited download links (7 days). Fetch promptly or mirror to your own storage.
attempt greater than 1 means the job was retried after an infrastructure
failure: a node died and another picked it up. The result is unaffected.
List jobs¶
| Param | Default | Notes |
|---|---|---|
status |
all | queued · running · succeeded · failed · canceled |
limit |
25 | max 100 |
cursor |
, | pass next_cursor from the previous page |
{ "data": [ { "id": "job_...", "status": "succeeded", ... } ],
"next_cursor": "MjAyNi0wOC0wOVQ..." }
Newest first. Pagination is cursor-based, so pages stay stable while new jobs are
being submitted. next_cursor is null on the last page.
Endpoint status¶
{
"endpoint_id": "ep_...",
"in_flight": { "queued": 3, "running": 8 },
"last_24h": { "succeeded": 412, "failed": 2, "canceled": 1 },
"workers": { "total": 8, "ready": 8, "stale": 0, "nodes": ["nyc1-5090-01"] },
"oldest_queued_seconds": 4.2
}
Use in_flight.queued and oldest_queued_seconds to decide whether to throttle
submissions. workers.stale counts slots that have stopped heartbeating, if it
is non-zero, capacity is degraded.
Who am I¶
{ "account": { "id": "acct_...", "name": "Your Company" },
"scope": null,
"endpoints": [ { "id": "ep_photo", "name": "Photo", "status": "active" } ] }
scope is null for an
account-wide key, or an endpoint ID if the key reaches only that one.
Endpoints¶
GET /v1/endpoints list, with requested vs assigned GPUs
POST /v1/endpoints create
PATCH /v1/endpoints/{id} rename, or change the GPU count
DELETE /v1/endpoints/{id} retire and release its GPUs
POST { "name": "Wan2 video", "replicas": 350, "image": "yourrepo/wan2:2026-08-11" }
-> 201 { "id": "ep_...", "name": "Wan2 video", "status": "active",
"requested_replicas": 350, "assigned_replicas": 350,
"image_ref": "yourrepo/wan2:2026-08-11" }
replicas is the number of GPUs you want. image is what they run, and it can
be set here or later with a deployment. An endpoint without an image holds its
GPUs but cannot answer a job, so set it unless you mean to deploy separately.
Unknown fields are rejected with 422 naming the field. A typo fails loudly
instead of returning success for something we did not do.
assigned_replicas is what the fleet
could actually give, so a shortfall is visible rather than silent.
For demand-driven capacity, provide capacity instead of replicas:
{
"name": "usage-driven model API",
"capacity": {
"minimum_ready": 0,
"maximum_ready": 20,
"idle_timeout_seconds": 300
}
}
Queued work raises the requested slot count within those bounds. Once the
endpoint has drained and remained idle for the configured interval, it returns
to minimum_ready. Setting minimum_ready above zero keeps warm capacity.
This policy currently assigns GPU slots already connected to the tenant fleet;
provider-side machine acquisition is a separate infrastructure layer.
Capacity is optional. Existing endpoints and requests using replicas remain
fixed-capacity, and patching replicas switches an elastic endpoint back to
fixed capacity. replicas and capacity cannot be supplied together.
Changing replicas re-assigns GPUs. A GPU moving between endpoints finishes its
current job first, then pulls and loads the new image, so nothing in flight is
interrupted.
DELETE is refused with 409 while jobs are still in flight on that endpoint.
POST with an id you choose is allowed as long as it starts with ep_.
Model deployment versions¶
Your endpoint runs one active version. Every deploy saves a new version, making history and rollback safe.
POST /v1/endpoints/{endpoint_id}/deployments
{
"model": {
"source": "huggingface",
"id": "Qwen/Qwen3-0.6B",
"revision": "<commit-sha>"
}
}
{ "id": "dep_...", "image_ref": "registry.example.com/model@sha256:...",
"active": true, "workers_total": 8, "workers_on_this": 0 }
For a public Hugging Face model, Nestor selects its tested vLLM image and
serving configuration. Advanced integrations may still supply the complete
spec; the legacy image-only request remains compatible with ComfyUI.
See Model deployments for complete examples.
OpenAI-compatible chat¶
Send the standard OpenAI chat-completions JSON body. The response is the model
server's OpenAI-compatible response. Nestor adds X-Nestor-Job-Id,
X-Nestor-Deployment-Id, and X-Nestor-Image-Digest headers.
This endpoint currently supports non-streaming requests. stream: true is
rejected rather than buffering a completed response and presenting it as live
token streaming.
Deployment responses also expose runtime convergence for live assigned workers:
workers_starting, workers_ready, workers_failed, and bounded
rollout_errors. These describe readiness on the exact revision, while the
older workers_on_this field continues to describe container/image rollout.
Generic HTTP runtime¶
runtime.driver: "generic_http" runs a JSON-over-HTTP inference container.
The deployment supplies the container port and readiness settings normally,
plus these driver-owned values under runtime.config:
The driver sends the submitted job input as the JSON request body. A
successful JSON-object response becomes the job output unchanged. Inference
and readiness paths must be relative container paths beginning with /.
Non-2xx responses, timeouts, connection failures, and invalid JSON become
structured job errors. Authentication headers, streaming responses, binary
responses, and automatic artifact extraction are not part of this driver.
Rollout is drain-and-roll. Each GPU finishes its current job, then pulls and switches. No running job is ever interrupted, so a deploy takes roughly the longest in-flight job plus the image pull. Capacity degrades gradually rather than all at once, during the roll, some jobs run the old image and some the new, and each job records which.
Watch progress with workers_on_this / workers_total, or on the Capacity tab.
GET /v1/endpoints/{id}/deployments history, newest first
POST /v1/endpoints/{id}/deployments/{dep_id}/activate roll back to a revision
Rollback is just activating an earlier revision, using the same drain-and-roll.
Notes:
- Pin a digest for reproducibility. A tag is mutable; repo/img@sha256:...
is not. image_digest on each job records exactly what ran.
- A failed pull leaves that worker on the old image rather than going dark.
Watch workers_on_this. If it stops climbing, the pull is failing.
- Private registries authenticate with a read-only pull token stored on your
endpoint via PUT /v1/endpoints/{id}/registry.
Cancel¶
Guaranteed for queued jobs: they transition tocanceled immediately and never
run. Best-effort for jobs already executing: the worker is signalled on its next
heartbeat and interrupts, but a job that finishes first still succeeds. Always
check the returned status rather than assuming.
Webhooks¶
On every terminal state we POST to your webhook_url:
{
"id": "evt_...",
"type": "job.succeeded", // job.failed | job.canceled
"data": { "job": { "id": "job_...", "status": "succeeded",
"output": { "artifacts": [ ... ] },
"metadata": { "customer_job_id": "84729" } } }
}
Headers:
Delivery is at-least-once with a 15s timeout and exponential-backoff retries
(10 attempts over ~2 hours). Deduplicate on Nestor-Webhook-Id.
Verifying the signature¶
Sign over the raw request body, before any JSON parsing. Re-serialising changes the bytes and the signature will not match.
Python (FastAPI)
import hashlib, hmac, os
from fastapi import FastAPI, Header, HTTPException, Request
SECRET = os.environ["NESTOR_WEBHOOK_SECRET"]
app = FastAPI()
@app.post("/nestor/webhook")
async def hook(
request: Request,
nestor_webhook_id: str = Header(...),
nestor_webhook_timestamp: str = Header(...),
nestor_webhook_signature: str = Header(...),
):
raw = await request.body() # raw bytes, not the parsed dict
msg = f"{nestor_webhook_id}.{nestor_webhook_timestamp}.".encode() + raw
expected = "v1=" + hmac.new(SECRET.encode(), msg, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, nestor_webhook_signature):
raise HTTPException(401, "bad signature")
# dedupe on nestor_webhook_id; this may be a redelivery
return {"ok": True}
Node (Express)
const crypto = require("crypto");
// mount the raw body for this route only
app.post("/nestor/webhook", express.raw({ type: "application/json" }), (req, res) => {
const id = req.get("Nestor-Webhook-Id");
const ts = req.get("Nestor-Webhook-Timestamp");
const sig = req.get("Nestor-Webhook-Signature");
const msg = Buffer.concat([Buffer.from(`${id}.${ts}.`), req.body]);
const expected = "v1=" + crypto.createHmac("sha256", process.env.NESTOR_WEBHOOK_SECRET)
.update(msg).digest("hex");
if (expected.length !== sig.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).end();
}
res.json({ ok: true }); // respond 2xx fast; do work asynchronously
});
Respond 2xx quickly. Anything else, or a response slower than 15s, is treated
as a failure and retried.
Errors¶
Failed jobs carry a structured error:
| Code | Meaning | Retry? |
|---|---|---|
invalid_input |
Input does not satisfy the selected runtime | Fix the request |
workflow_rejected |
ComfyUI would not accept the workflow, usually a missing model or a bad node | Fix the workflow |
execution_error |
The workflow ran and failed. detail.messages carries ComfyUI's output |
Depends |
no_outputs |
Completed but produced no files, usually a missing Save node | Fix the workflow |
timeout |
Exceeded the per-job wall-clock limit | Retry. The limit is configurable per endpoint |
runtime_unreachable |
The container was not reachable | Automatic, infra |
worker_lost |
Node died and attempts were exhausted | Automatic, infra |
agent_error |
Unexpected node-side error | Automatic, infra |
HTTP-level: 401 bad key · 403 key not valid for that endpoint · 404 unknown
job · 422 malformed body or bad query parameter.
Infrastructure failures may be retried under the job lease policy. Job-level failures are not automatically made correct by retrying. Runtime drivers decide whether an error describes invalid input or infrastructure.
Practical notes¶
- One job per worker at a time. This is the current worker-model limitation, independent of runtime.
- Warmth is policy. A newly started worker pays image, model-load and compilation costs. A positive capacity minimum avoids repeated cold starts; a zero minimum trades latency for idle-cost savings.
- Idempotency keys are the safe way to retry a submission whose response you did not see.
- Long jobs are fine. Heartbeats keep a lease alive for as long as the job runs; the per-job ceiling is a configured wall-clock limit, not a request timeout.
- Keep the API key server-side. It can submit jobs. Keys are revocable and replaceable without downtime.