Skip to content

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

GET /v1/jobs/{job_id}
{
  "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

GET /v1/endpoints/{endpoint_id}/jobs?status=&limit=&cursor=
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

GET /v1/endpoints/{endpoint_id}/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

GET /v1/me
{ "account": { "id": "acct_...", "name": "Your Company" },
  "scope": null,
  "endpoints": [ { "id": "ep_photo", "name": "Photo", "status": "active" } ] }
Your account and every endpoint the key can reach. Use it to discover endpoint IDs and to verify a key without submitting anything. 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:

{
  "inference_path": "/infer",
  "method": "POST"
}

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

POST /v1/jobs/{job_id}/cancel
Guaranteed for queued jobs: they transition to canceled 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:

Nestor-Webhook-Id: evt_...
Nestor-Webhook-Timestamp: 1723100000
Nestor-Webhook-Signature: v1=<hex>

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:

{ "error": { "code": "execution_error", "message": "...", "detail": { } } }

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.