X3DStudios

Quote a model

Price a part before you order it: /api/price returns a full breakdown from a bounding box, and /api/slice-quote measures a stored model with a real slice.

Two endpoints put a price on a model before you commit to printing it. POST /api/price is open to anyone and turns a bounding box into a full cost breakdown. POST /api/slice-quote runs a real OrcaSlicer slice of a model we already hold for you — but it is authenticated by a browser session, not by an API key.

Only one of these is callable with an API key
/api/price needs no authentication at all. /api/slice-quote requires a signed-in session cookie and a generation id from your own account, so it cannot be driven from a server that holds only an x3d_live_ key. There is no key-authenticated slicing endpoint.

Estimate from dimensions#

POST/api/priceNo auth

Price a part from its bounding box, material, quality and quantity.

Request body (JSON)
bboxMm.xnumberrequired
Width in millimetres. Must be a finite number greater than zero — a numeric string is rejected.
bboxMm.ynumberrequired
Depth in millimetres, same rules.
bboxMm.znumberrequired
Height in millimetres, same rules.
materialstringoptionaldefault pla
Sets the per-gram rate and the density used to turn volume into weight. Anything else is a 400.plapetgabsasatpu
qualitystringoptionaldefault standard
standard is 0.2 mm layers at 1x the rate; premium is 0.1 mm layers at 1.75x.standardpremium
quantitynumberoptionaldefault 1
Rounded, and floored at 1. Five or more units take 5% off, ten or more take 10%.
colorstringoptional
A stock spool name or hex. Trimmed to 64 characters and dropped entirely if it contains anything outside letters, digits, spaces and # ( ) , . _ -. Off-palette colours price at the $0.20/g floor.
Request
curl -X POST https://x3dstudios.com/api/price \
  -H "Content-Type: application/json" \
  -d '{"bboxMm":{"x":60,"y":40,"z":20},"material":"pla","quality":"standard","quantity":1,"color":"Black"}'
200
{
  "filamentChanges": 0,
  "multiColorGrams": 0,
  "multiColorHours": 0,
  "grams": 27,
  "printHours": 1.32,
  "materialCost": 3.24,
  "machineCost": 0,
  "shipping": 7,
  "baseFee": 0,
  "perUnit": 3.24,
  "subtotal": 3.24,
  "total": 10.24,
  "currency": "USD",
  "material": "pla",
  "quality": "standard",
  "quantity": 1,
  "color": "Black",
  "customColor": false,
  "pricePerGram": 0.12
}
Response fields
gramsnumberoptional
Billable weight, rounded up to the whole gram.
printHoursnumberoptional
Estimated machine time. Informational unless machineCost is non-zero.
materialCostnumberoptional
grams x pricePerGram x the quality factor. On a single-colour part this is the whole price.
machineCostnumberoptional
Tool-change surcharge, at $1.50/hr. Always 0 from this endpoint: there is no multi-colour input to it, and the farm prints single-colour today, so nothing here bills machine time on top of the per-gram rate.
perUnitnumberoptional
Price of one unit, never below the $1.00 minimum.
subtotalnumberoptional
perUnit x quantity with the bulk discount applied, floored at $1.00 per unit.
shippingnumberoptional
Always the $7 flat domestic rate here. It does not apply the free-shipping threshold and knows nothing about the destination country.
baseFeenumberoptional
Always 0. There is no order fee.
totalnumberoptional
subtotal + shipping + baseFee. See the warning below before you show this to a customer.
pricePerGramnumberoptional
The rate actually used, after the custom-colour floor.
customColorbooleanoptional
true when the colour is not one we stock, which is why pricePerGram may read $0.20.
filamentChangesnumberoptional
Modelled filament changes. Always 0 today — the field is there for a multi-colour plate the farm cannot yet print.
multiColorGramsnumberoptional
Purge waste attributed to those changes, in grams. Always 0, for the same reason.
multiColorHoursnumberoptional
Tool-change time attributed to those changes, in hours. Always 0, for the same reason.
colorstringoptional
The colour that was priced. Omitted from the response when you sent none.
total is not what an order will charge
This endpoint adds the flat $7 unconditionally. A real order applies the thresholds: US postage is $7 flat and free at a $50 subtotal, international is $30 flat and free at $500. Build your own total from subtotal and the rule for the destination.
StatuserrorWhen
400invalid json bodyThe body did not parse as JSON.
400bboxMm.x/y/z (mm) requiredA dimension is missing, not a number, not finite, or not greater than zero.
400model exceeds the 340×320×340mm build volumeThe three dimensions, sorted largest first, do not fit the plate. A long thin part that fits rotated is not rejected.
400unknown material: xyzmaterial is not one of the five keys.
400unknown quality: xyzquality is not standard or premium.

Measure a stored model with a real slice#

POST/api/slice-quoteSigned in

Slice a model on your account for real and cache the measurement on it.

This runs the same engine the farm prints with — OrcaSlicer on a Bambu profile — against a model already in your X3D account, and stores the result so every later quote of that model uses it. It is idempotent: a model that has been sliced returns the stored measurement instead of slicing again.

Request body (JSON)
generationIdstringrequired
The id of a generation on your own account. Anything else, including a model belonging to someone else, returns 404.
From a signed-in page
const res = await fetch("/api/slice-quote", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  credentials: "include",
  body: JSON.stringify({ generationId: "gen_abc123" }),
});

const data = await res.json();
if (res.ok) {
  const { filamentCm3, seconds, layers } = data.sliceAnalysis;
  console.log(data.cached ? "cached" : "freshly sliced", filamentCm3, seconds, layers);
}
200
{
  "ok": true,
  "cached": false,
  "sliceAnalysis": {
    "filamentCm3": 17.2,
    "seconds": 5040,
    "layers": 214,
    "sliceScale": 1,
    "slicedAt": "2026-09-09T12:00:00.000Z"
  }
}

filamentCm3 is material-independent — multiply it by the density of the material you are ordering (PLA 1.26, PETG 1.27, ABS 1.04, ASA 1.07, TPU 1.21 g/cm3) to get grams, then round up to the whole gram and multiply by the rate. seconds is the slicer's own time estimate and layers is its layer count. sliceScale records the scale the slice ran at; it is 1 for a slice made through this endpoint.

StatuserrorWhen
401Authentication requiredNo signed-in session. An API key does not satisfy this.
429Too many slice requests — try again shortly.More than 30 requests in an hour from one account.
400generationId requiredMissing or non-string generationId.
404Generation not foundNo such generation on your account, or it has no model file.
503Slicer not configuredThe slicing service is not available in this deployment.
422Model is not in managed storageThe model is not held in X3D storage, so we cannot fetch it to slice.
422Only STL/OBJ/3MF models can be slicedGLB models keep the estimate — the slicer reads meshes, not glTF containers.
502Slicing failed — quote stays on the estimateThe slice did not complete. The estimate is still usable.
A stored slice is returned as soon as the generation is found — before the slicer checks below it — so a previously sliced model answers even when the slicer is down.

Why a slice differs from an estimate#

An estimate is arithmetic on geometry: volume times a fixed model of solid walls and sparse infill, times the material's density. A slice is what the machine will actually extrude. The two disagree in both directions, and the disagreement is not small.

InputWhat it assumesWhere it goes wrong
Bounding box (/api/price)The part fills its box, then 30% solid walls with 20% infill in the rest.Reads high for anything that is not a block. A bracket occupying a third of its box is quoted at roughly three times its weight.
Mesh volumeReal enclosed volume, then the same wall-and-infill model.Reads low for thin, wall-dominated parts, which are nearly all perimeter: a stencil estimated at 12.9 g weighs 21 g on the plate.
Real slice (/api/slice-quote)Nothing. It reads the filament the g-code actually uses and the time it actually takes.Costs 10–60 seconds, needs the model in managed storage, and cannot run on GLB.
  • A slice counts supports, brim, skirt and prime line. No estimate models any of them.
  • A slice counts purge: every filament change flushes the hotend, so a four-colour plate can burn three times the filament of the same model in one colour.
  • Billable weight rounds up to the whole gram either way, which is also what Bambu Studio displays — so a quote and a slicer preview agree on the number.
  • Where a measured slice exists, we bill it. That is the point: the grams charged are the grams that leave the nozzle.
You do not have to quote at all
The submit response returns estimatedCost and a breakdown of grams, hours, print and postage for the order it just created — priced from the real file, and from the slicer's own numbers when you send an already-sliced .gcode or .gcode.3mf.