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, at 100% or at any size from 50% to 300%.
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 the browser, 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.
shippingSpeedstringoptionaldefaultstandard- standard is $7 and 4–6 business days in transit; express is $15 and 2–3, and is a US-only service. Anything unrecognised is quoted as standard rather than refused — a quote is read-only, and the value comes back in shippingSpeed so you can see which was used.
standardexpress
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,
"shippingSpeed": "standard",
"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: it has no colour input and prices one colour. A print in colour is priced on /print from a real slice of it in its colours, whose grams and hours already contain every change.
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- The flat domestic rate for the requested speed: $7 standard, $15 express. It does not apply the free-shipping threshold and knows nothing about the destination country.
shippingSpeedstringoptional- Which service that rate buys — "standard" or "express". Echoes back what you asked for, so a value this endpoint did not recognise is visible as "standard" rather than silently priced as one.
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: colour prices come from a real slice, never from modelled changes, and the model that would fill this is switched off.
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, or one this browser uploaded signed out, for real and cache the measurement on it.
This runs the same engine the farm prints with — OrcaSlicer on the Bambu H2S 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. A GLB you uploaded (or an exact edit of one) is sliced with glTF's up axis, Y, as the printer's up, lying the way the page shows it, which is how the farm prints it. The one exception is a GLB plainly written Z-up, against glTF's convention (a flat base facing its -Z, as some STL-to-GLB converters leave it): it is sliced standing on that base, with its Z as the printer's up, as the farm prints it, although the page shows it on its side. A model generated here is measured in the pose the farm prints it in, as it always has been.
Each size is its own measurement. With scalePercent the model is scaled to that size before the slicer runs and the result is stored beside the 100% one, so asking for the same size again answers from storage. An order at that size is charged on exactly this slice: the order routes slice it themselves when it is missing, and refuse the order while it cannot be measured.
generationIdstringrequired- The id of a generation on your own account. Anything else, including a model belonging to someone else, returns 404.
scalePercentintegeroptionaldefault100- The size to measure at, as a whole percent from 50 to 300: the same number an order's scalePercent takes. Anything else is a 400. A file you sliced yourself and a Bambu Studio or OrcaSlicer project are measured at 100% only, and any other size answers 409.
materialstringoptionaldefault"pla"- The material you are pricing: pla, petg, abs, asa, tpu or custom. Anything else is a 400. tpu is sliced on the farm's own TPU preset (Generic TPU), which lays down a little more plastic than the PLA profile, so it has measurements of its own at every size, 100% included, and they count against the per-size limit below. The other materials are priced on the PLA measurement. With custom, also send customFilamentName: a name that states TPU is measured as tpu.
const res = await fetch("/api/slice-quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ generationId: "gen_abc123", scalePercent: 200 }),
});
const data = await res.json();
if (res.ok) {
const { filamentCm3, seconds, layers } = data.sliceAnalysis;
console.log(data.scalePercent, data.cached ? "cached" : "freshly sliced", filamentCm3, seconds, layers);
} else if (data.measuring) {
// Nothing was learned about the model this time. Ask again in a little while.
}{
"ok": true,
"cached": false,
"sliceAnalysis": {
"filamentCm3": 100.6,
"seconds": 19700,
"layers": 428,
"sliceScale": 2,
"slicedAt": "2026-10-08T12:00:00.000Z"
},
"scalePercent": 200
}filamentCm3 is filament volume — 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.24 g/cm3) to get grams, then round up to the whole gram and multiply by the rate. Ask with the material you are ordering: a tpu measurement is TPU's own, and only it prices a TPU order. seconds is the slicer's own time estimate and layers is its layer count. sliceScale is the factor the slice ran at: 1 at 100%, 2 at 200%. scalePercent echoes the size you asked for.
| Status | error | When |
|---|---|---|
| 401 | We could not recognise this browser. Allow cookies for this site, or sign in to continue. | Comes with signIn: true. No signed-in session and no visitor cookie we issued. An API key does not satisfy this. |
| 429 | Too many price checks from this browser in the last hour. … | Signed out only, with signIn: true and Retry-After. A signed-out browser may weigh 20 times an hour, a connection 40 an hour and 120 a day, and everyone signed out together 1500 a day. The message says which limit was hit; signing in lifts them. |
| 429 | Too many slice requests — try again shortly. | At 100%: more than 30 requests in an hour from one account that could not be answered from storage. |
| 429 | Too many sizes weighed in the last hour. Try this one again later, or pick a size you have already priced. | Any other size, and tpu at any size: more than 30 new sizes sliced in an hour for one account, counting every page that weighs one, ordering included. Only a slice that reached the slicer's verdict counts; an attempt it was too busy for is given back. Comes with Retry-After. Sizes already weighed still answer. |
| 400 | generationId required | Missing or non-string generationId. |
| 400 | material must be one of the material keys. | material is not pla, petg, abs, asa, tpu or custom. |
| 400 | scalePercent must be a whole number from 50 to 300. | scalePercent is a fraction, out of range, or not a number. |
| 400 | At 300% this model is 410 mm on its longest side, more than the 340 mm we can print. | Comes with code too-big. No order accepts a part longer than 340 mm, so that size is not sliced and does not count against the limit. |
| 404 | Generation not found | No such generation on your account, or in this signed-out browser's uploads, or it has no model file. |
| 409 | The reason, with code sliced-scale or project-scale | A size other than 100% for a file you sliced yourself, or for a Bambu Studio or OrcaSlicer project. Both print at their own size. |
| 422 | The reason, with failed: true | The slicer answered that it could not slice the model at that size, or for tpu in TPU. Only that verdict is kept, for ten minutes, then the size is tried again. Other sizes are unaffected. |
| 503 | We could not weigh this size just now. Try again in a moment. | Comes with measuring: true. The file could not be read, or the slicer was busy, down or timed out, so nothing was learned about the model at that size and nothing is kept. Ask again later. |
| 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/GLB models can be sliced | The model is in another format. A GLB is converted to STL on its way to the slicer. |
| 422 | The slicer's reason, with failed: true | At 100%, the slicer refused the model (most often a damaged mesh). The estimate is still usable; the model cannot be ordered until it is repaired and uploaded again. |
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 per size and needs the model in managed storage. |
- 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.
Questions
Can I call /api/slice-quote with my API key?
No. It is authenticated by the browser: a signed-in session, or the visitor cookie a signed-out browser is given when it uploads a model at /print. It only works on a model stored for that account or that browser. A server holding an API key can get a real slice another way: the MCP server's quote_print tool takes the key and slices the file. See /docs/api/mcp.
Why is the /api/price figure higher than what my order cost?
It prices the whole bounding box as if the part filled it, because that is all you gave it. A part that does not fill its box weighs far less, and an order is priced from the real geometry — and, where we can slice it, from the actual filament the slice uses.
Does a model at 200% use eight times the plastic?
No, and nothing here prices it as if it did. Doubling every side makes the volume eight times bigger, but walls and top and bottom skins keep their thickness and only the sparse infill grows. One model measured 5.85 times the filament at 200% and 17.3 times at 300%. Ask /api/slice-quote with scalePercent for the size you want and it slices the model at that size.
Why can a real slice come out heavier than the estimate?
The estimate is volume times a fixed wall-and-infill model. A thin, wall-dominated part is almost entirely perimeter, which that model cannot see: a stencil that estimates at 12.9 g weighs 21 g on the plate. The slice also counts supports, brim and purge.
Related guides
- Why Is 3D Printing So Expensive? What Drives the CostWhy is 3D printing so expensive? Billable grams × the 3D printing price per gram × a layer multiplier, with a $1 minimum per part, run on four real orders.
- Build 3D Printing Quote Software: Price a Print in CodeBuild 3D printing quote software in about fifteen lines: mesh volume to billable grams times the per-gram rate, or POST a bounding box to the quote API.
- AI 3D Model Quality: Why Prints Look UncannyAI 3D model quality, tested in plastic: why AI 3D models look right on screen and wrong in your hand, from baked textures to the 0.4 mm feature floor.