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.
Estimate from dimensions#
/api/priceNo authPrice a part from its bounding box, material, quality and quantity.
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.
materialstringoptionaldefaultpla- Sets the per-gram rate and the density used to turn volume into weight. Anything else is a 400.
plapetgabsasatpu qualitystringoptionaldefaultstandard- standard is 0.2 mm layers at 1x the rate; premium is 0.1 mm layers at 1.75x.
standardpremium quantitynumberoptionaldefault1- 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.
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"}'{
"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
}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.
| Status | error | When |
|---|---|---|
| 400 | invalid json body | The body did not parse as JSON. |
| 400 | bboxMm.x/y/z (mm) required | A dimension is missing, not a number, not finite, or not greater than zero. |
| 400 | model exceeds the 340×320×340mm build volume | The three dimensions, sorted largest first, do not fit the plate. A long thin part that fits rotated is not rejected. |
| 400 | unknown material: xyz | material is not one of the five keys. |
| 400 | unknown quality: xyz | quality is not standard or premium. |
Measure a stored model with a real slice#
/api/slice-quoteSigned inSlice 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.
generationIdstringrequired- The id of a generation on your own account. Anything else, including a model belonging to someone else, returns 404.
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);
}{
"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.
| Status | error | When |
|---|---|---|
| 401 | Authentication required | No signed-in session. An API key does not satisfy this. |
| 429 | Too many slice requests — try again shortly. | More than 30 requests in an hour from one account. |
| 400 | generationId required | Missing or non-string generationId. |
| 404 | Generation not found | No such generation on your account, or it has no model file. |
| 503 | Slicer not configured | The slicing service is not available in this deployment. |
| 422 | Model is not in managed storage | The model is not held in X3D storage, so we cannot fetch it to slice. |
| 422 | Only STL/OBJ/3MF models can be sliced | GLB models keep the estimate — the slicer reads meshes, not glTF containers. |
| 502 | Slicing failed — quote stays on the estimate | The slice did not complete. The estimate is still usable. |
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.
| Input | What it assumes | Where 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 volume | Real 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.