NDNCI Docs
API v1

Jobs and progress

Receive immediate results or follow queued work through polling, webhooks and SSE.

Immediate and queued results

POST https://api.ndnci.com/v1/runs returns an immediate result for synchronous operations. A queued operation returns HTTP 202, a jobId, its initial status and the bounded maximum cost reserved for execution.

The jobs list contains metadata only. Use GET https://api.ndnci.com/v1/jobs/{jobId} to retrieve its current status and bounded inline output. The job moves through pending and processing to completed, failed, or cancelled.

curl --fail-with-body "https://api.ndnci.com/v1/jobs/$NDNCI_JOB" \
  -H "Authorization: Bearer $NDNCI_KEY" \
  -H "X-Ndnci-Workspace: $NDNCI_WORKSPACE"

Poll with increasing delays and randomized backoff. Honor Retry-After rather than opening a tight retry loop. Queue capacity helps you schedule additional work within the workspace's limits.

Retrieve complete output

Job snapshots keep ordinary result fields within 48 KB and the complete inline projection, including file links, within 60 KB. Large fields are identified in output.omittedFields.

Use complete job output, GET https://api.ndnci.com/v1/jobs/{jobId}/output, to retrieve the complete public result. It is available with jobs:read for Free, Starter and Pro, and consumes two usage points per page. The MCP tool is ndnci_jobs_output, with the same jobId and optional cursor.

Each response contains a JSON string fragment of at most 8192 UTF-8 bytes, its byte offset, the document's total size and SHA-256 digest, and the next cursor. Fragments end on Unicode boundaries. Join them in order and call JSON.parse once after the final page. The full result is bounded to 16 MiB. Private provider settings, worker checkpoints and backend object keys are excluded.

import { createHash } from "node:crypto";
import { setTimeout as wait } from "node:timers/promises";

const headers = {
  Authorization: `Bearer ${process.env.NDNCI_KEY}`,
  "X-Ndnci-Workspace": process.env.NDNCI_WORKSPACE,
};
const fragments = [];
let cursor;
let offset = 0;
let digest;
let total;
let snapshotExpiresAt;

while (true) {
  const url = new URL(
    `https://api.ndnci.com/v1/jobs/${process.env.NDNCI_JOB}/output`,
  );
  if (cursor) url.searchParams.set("cursor", cursor);
  const response = await fetch(url, { headers });
  if (response.status === 429 || response.status === 503) {
    const seconds = Number(response.headers.get("Retry-After"));
    const resumeAt = Date.now() + seconds * 1000;
    if (
      !Number.isSafeInteger(seconds) ||
      seconds < 1 ||
      !Number.isSafeInteger(resumeAt)
    )
      throw new Error(
        "Retry-After is unavailable; check usage before retrying.",
      );
    if (snapshotExpiresAt && resumeAt >= Date.parse(snapshotExpiresAt))
      throw new Error(
        "Quota resets after this snapshot expires; restart later.",
      );
    while (true) {
      const remaining = resumeAt - Date.now();
      if (remaining <= 0) break;
      await wait(Math.min(remaining, 60_000));
    }
    continue;
  }
  if (!response.ok)
    throw new Error(`Output request failed: ${response.status}`);
  const { data } = await response.json();
  if (data.offsetBytes !== offset || (digest && digest !== data.sha256))
    throw new Error("Output snapshot changed; restart from the first page.");
  digest = data.sha256;
  total = data.totalBytes;
  snapshotExpiresAt = data.snapshotExpiresAt;
  fragments.push(data.fragment);
  offset += Buffer.byteLength(data.fragment, "utf8");
  cursor = data.nextCursor;
  if (!cursor) break;
}

const serialized = fragments.join("");
if (
  offset !== total ||
  createHash("sha256").update(serialized).digest("hex") !== digest
)
  throw new Error("Output integrity check failed.");
const output = JSON.parse(serialized);

A cursor expires after thirty minutes and remains bound to the same access grant, workspace, job and result. Every page checks current authorization. A changed result returns 409; an expired cursor returns 410. Restart without a cursor and discard fragments from the previous snapshot. Honor 429 and Retry-After when a limit is reached.

Sprite output includes complete engine exports, atlas, strips and individual frame URLs reconstructed from owned result records. Asset URLs share one fixed signing time while you paginate; begin a new output request to renew them. Generated files are also exposed as owned media metadata and temporary download links.

Retrieve generated files

Completed output follows the selected Tool operation's public contract. For operations that generate files, use the returned workspace media identifiers and the media download endpoint. Download URLs are temporary; request a new URL when one expires.

Jobs and related credit transactions include executionOrigin when known: web, api, mcp, or system. Historical rows may contain null; that means their origin was not recorded.

Cancel or retry

Call POST https://api.ndnci.com/v1/jobs/{jobId}/cancel to request cancellation. Pending work releases unused reservations when cancellation completes. If a provider has already accepted work, its committed consumption may still be charged.

Retry an eligible failed job with POST https://api.ndnci.com/v1/jobs/{jobId}/retry and a new Idempotency-Key. NDNCI rechecks permissions, queue capacity, credits and budget ceilings, and preserves completed checkpoints where the operation supports them.

API and MCP retries use NDNCI credits. Retry a job funded by a personal provider key from the Web application; public access rejects that funding mode with 409.

Idempotency keys protect against repeated admission of the same mutation. Reuse a key only for the same operation and input; a conflicting reuse returns HTTP 409.

Stream progress with SSE

Pro workspaces can subscribe to a bounded server-sent event stream for a job:

curl --no-buffer --fail-with-body \
  "https://api.ndnci.com/v1/jobs/$NDNCI_JOB/stream" \
  -H "Authorization: Bearer $NDNCI_KEY" \
  -H "X-Ndnci-Workspace: $NDNCI_WORKSPACE" \
  -H 'Accept: text/event-stream'

Keep only the streams your interface needs and close them when the view is no longer active. Connection limits apply to keys, workspaces and the service. Read current stream capacity from the usage endpoint.

For a custom browser interface, use an authenticated fetch stream that can send headers. Do not put a key into a query string. After a disconnect, read the job snapshot again before resubscribing.

Receive completion events

Signed webhooks notify your application of job outcomes without a persistent connection. They include an event identifier for deduplication, retries and controlled replay. Local forwarding supports receivers running on your development machine.

On this page