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 runs on dedicated, always-warm GPU workers. Jobs are queued and dispatched to the first free worker, no cold starts between jobs.
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":{"workflow":{ ...ComfyUI API-format JSON... }}}'
# -> 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":{"artifacts":[{"url":"https://..."}]}}
New here? Getting started is a full walkthrough from receiving your key to a finished image in your own storage.
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.
Getting the workflow JSON: in ComfyUI, enable Settings → Enable Dev mode
options, then use Save (API Format). That is the exact shape to put in
input.workflow. The regular "Save" format will be rejected.
Submit a job¶
POST /v1/endpoints/{endpoint_id}/jobs
Content-Type: application/json
Idempotency-Key: your-unique-id (optional but recommended)
{
"input": {
"workflow": { "...": "ComfyUI API-format workflow JSON" }
},
"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:
- 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.
artifacts[].type is image, video, audio or file, derived from the
content type. A workflow that emits video or audio returns it here the same way.
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_.
Deployments and immutable workload revisions¶
Your endpoint runs one active deployment revision. Deploying a new one is an API call, not a support request.
POST /v1/endpoints/{endpoint_id}/deployments
{ "image": "yourrepo/comfyui:2026-08-11", "notes": "adds upscaler nodes" }
{ "id": "dep_...", "image_ref": "yourrepo/comfyui:2026-08-11",
"active": true, "workers_total": 8, "workers_on_this": 0 }
The image-only request remains compatible and creates the historical ComfyUI
configuration. New integrations may send spec instead. A spec includes its
runtime driver and container, ports and readiness, safe environment values and
secret references, intrinsic resources, and execution limits. Responses retain
image_ref and image_digest and additionally return the complete spec.
Changing material configuration creates another revision; revisions cannot be
updated in place.
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.workflow missing or not an object |
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 are retried for you (the job returns to the queue and another GPU picks it up, with the seed preserved). Job-level failures are not, retrying a bad workflow just fails twice.
Practical notes¶
- One job per worker at a time. Nestor's queue stays authoritative; we never push more than one workflow into a container at once, so ComfyUI's own queue is never a hidden second buffer.
- Warm workers. The first job on a freshly started GPU pays a model load (tens of seconds). Every job after that skips it. Workers are not scaled to zero between jobs, so you do not pay that repeatedly.
- 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.