Printy3D

SDKs

TypeScript SDK

@printy3d/sdk wraps every /v1 endpoint with typed interfaces, one-call uploads, waiting, verified downloads, automatic idempotency keys and safe retries. ESM with type declarations, Node 18 or newer, no dependencies (it uses the global fetch).

Install#

shell
npm install @printy3d/sdk

Configure#

In Node the client reads PRINTY3D_API_KEY and, optionally, PRINTY3D_API_URL (default https://printy3d-api-staging.onrender.com). Create a key in the console: a p3d_test_ key runs free, simulated jobs for building your integration; a p3d_live_ key, available once your account is approved, runs the real engine.

ts
import { Printy3D } from "@printy3d/sdk";

const client = new Printy3D();                 // PRINTY3D_API_KEY, PRINTY3D_API_URL
const custom = new Printy3D({
  apiKey: process.env.PRINTY3D_API_KEY,
  timeoutMs: 150_000,                          // whole API request
  transferTimeoutMs: 120_000,                  // storage uploads and downloads
  maxRetries: 4,                               // retries of an idempotent request
  maxRetryDelayMs: 60_000,                     // longest Retry-After slept through
});
console.log((await client.org.retrieve()).name);

A first job#

ts
let job = await client.inspect("model.stl");    // upload + inspect + wait
console.log(job.analysis?.dimensions_mm, job.analysis?.is_watertight);

job = await client.repair("model.zip", { profile: "character", params: { printer: "mimaki" } });
const saved = await client.downloadArtifacts(job.id, "out/");   // every artifact, verified
for (const item of saved) console.log(item.artifact.label, item.path);

inspect, repair, tool and run take a path, bytes or a Blob (uploaded first), an uploaded File or a file_… id, create the job with a fresh Idempotency-Key and wait for it. Pass { wait: false } to get the queued job back at once.

Upload#

ts
const file = await client.upload("scan.zip");          // the File once it is "ready"
const fromBytes = await client.upload(bytes, { filename: "part.stl" });
const big = await client.upload("big.obj", {
  resumable: true,                                         // force the multipart upload
  onProgress: (sent, total) => console.log(`${sent}/${total}`),
  timeoutMs: 15 * 60_000,                                  // wait for verification
});

Files up to 10 MiB go up in one presigned PUT; larger files use the resumable multipart upload, which needs an approved account. The SDK computes the size and SHA-256, resumes missing parts for up to three rounds, completes the file and waits for verification. A rejected file throws FileRejectedError. Details: Uploads.

Inspect, repair and tools#

ts
const inspected = await client.inspect(file);
const recommended = inspected.analysis?.recommended;    // RepairParams for this file

await client.repair(file, { params: { scale: { mode: "target", value: 6, unit: "in" } } });
await client.repair(file, { profile: "mechanical", params: { preserve_shells: true } });

await client.tool(file, "cut", {
  planes: [{ origin: [0, 40, 0], normal: [0, 1, 0] }],
  keep_both_sides: true,
});
const measured = await client.tool(file, "measure", { point_a: [0, 0, 0], point_b: [10, 0, 0] });
console.log(measured.result);

await client.run(file, { recipe: "tool", tool: "convert", params: { target_format: "3mf" } });

client.tool is typed per tool: the parameters of "cut" are CutParams, of "lattice" LatticeParams, and so on. Every parameter is described on Repair and the Tools pages.

Waiting and progress#

ts
import { JobFailedError, WaitTimeoutError } from "@printy3d/sdk";

const queued = await client.repair(file, { wait: false });     // returns at once
const controller = new AbortController();

try {
  const job = await client.jobs.wait(queued.id, {
    timeoutMs: 2 * 3600_000,
    pollIntervalMs: 2000,
    onEvent: (e) => console.log(e.sequence, e.status, e.stage, e.progress),
    signal: controller.signal,
  });
} catch (e) {
  if (e instanceof JobFailedError) console.log("failed:", e.code, e.job.error_detail);
  else if (e instanceof WaitTimeoutError) console.log("still running server-side");
  else throw e;
}

jobs.wait polls through transient failures (no response, 429, 502–504), resolves on succeeded, throws JobFailedError or JobCancelledError (or resolves with the job when raiseOnFailure: false) and gives up with WaitTimeoutError after timeoutMs (default 4 hours; null waits forever). Every call takes an AbortSignal.

Download#

ts
for (const artifact of await client.artifacts.list(job.id)) {
  console.log(artifact.label, artifact.media_type, artifact.size_bytes);
}

await client.downloadArtifacts(job.id, "out/", { labels: ["result_glb", "result_obj_zip"] });
const path = await client.download(artifact, "out/");     // Node: a file or a folder
const bytes = await client.artifacts.read(artifact);       // Uint8Array, anywhere

Every download fetches a fresh presigned URL and checks the size and SHA-256 (throwing IntegrityError on a mismatch). Saving to disk is Node only; artifacts.read works everywhere.

Exports#

ts
const exported = await client.export(job.id, "stl");      // waits until "ready"
await client.download(exported.artifact!, "out/");

const pending = await client.export(job.id, "3mf", { wait: false }); // "ready" or "converting"

A format the job did not produce starts a charged conversion job (test keys: 501); a failed conversion throws ExportFailedError. Exports

Errors#

ts
import { Printy3DError } from "@printy3d/sdk";

try {
  await client.repair("model.stl");
} catch (e) {
  if (!(e instanceof Printy3DError)) throw e;
  if (e.code === "insufficient_credits") console.log(`${e.required} needed, ${e.available} available`);
  else if (e.code === "capacity_exhausted") console.log(`no capacity (${e.reason}); retry in ${e.retryAfter} s`);
  else if (e.code === "validation_failed") for (const f of e.errors) console.log(f.field, f.message);
  else console.log(e.status, e.code, e.detail, "request", e.requestId);
}

Every API error is a Printy3DError with status, code, title, detail, errors, requestId, retryAfter and the code-specific extras (required, available, requiredScope, reason). Other outcomes are subclasses:

ClassThrown when
APIConnectionErrorNo response (DNS, refused, reset) after retries.
APITimeoutErrorA request timed out; a write may have reached the API.
JobFailedErrorA wait saw failed; .job is the job.
JobCancelledErrorA wait saw cancelled.
WaitTimeoutErrorA wait gave up; the job keeps running.
FileRejectedErrorUpload verification rejected the file.
ExportFailedErrorAn export's conversion failed.
IntegrityErrorDownloaded bytes did not match size or SHA-256.
UploadIncompleteErrorUpload parts were still missing after every resume round.

Retries and idempotency#

The client retries a request only when repeating it is safe: GET, DELETE and the POSTs that carry an Idempotency-Key. It retries on no response, 429, 502–504 and 409 idempotency_in_progress with exponential backoff and jitter, or the server's Retry-After; a longer Retry-After than maxRetryDelayMs throws at once with retryAfter set. maxRetries: 0 turns retries off.

ts
// One key per logical operation: safe across process restarts.
await client.repair(file, { idempotencyKey: `order-${orderId}` });
await client.jobs.create({ input_file_id: file.id, recipe: "repair" }, { idempotencyKey: `order-${orderId}` });

API keys and credits#

ts
const issued = await client.apiKeys.create({
  name: "ci",
  scopes: ["files:write", "jobs:write", "jobs:read", "artifacts:read"],
});
const secret = issued.secret;                 // shown once; null on an idempotent replay
const rotated = await client.apiKeys.rotate(issued.api_key.id, { grace_seconds: 3600 });
await client.apiKeys.revoke(rotated.api_key.id);
for await (const key of client.apiKeys.iterate()) console.log(key.prefix, key.kind, key.revoked_at);

const balance = await client.balance.retrieve();
console.log(balance.available_credits, balance.low);
for await (const tx of client.balance.iterateTransactions()) console.log(tx.sequence, tx.kind);

All methods#

ResourceMethods
client.orgretrieve()
client.formatslist()
client.profileslist()
client.apiKeyslist(), iterate(), retrieve(), create(), rotate(), revoke()
client.filesupload(), create(), createResumable(), complete(), retrieve(), list(), iterate(), wait()
client.jobscreate(), retrieve(), list(), iterate(), cancel(), events(), iterateEvents(), wait()
client.artifactslist(), downloadUrl(), read(), download()
client.exportscreate(), retrieve(), wait()
client.balanceretrieve(), transactions(), iterateTransactions()
clientupload(), run(), inspect(), repair(), tool(), download(), downloadArtifacts(), export()

Types#

Every request and response shape is exported as an interface with the API's field names (Job, File, Artifact, Analysis, RepairParams, CutParams, Export, Balance, …). Unknown fields the API adds later pass through untouched.