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 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

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.

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

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_.


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

POST /v1/endpoints/{endpoint_id}/openai/chat/completions

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:

{
  "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 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.