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, 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.

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, or the visitor cookie /print gives a signed-out browser, and a generation id that belongs to that account or browser, so it cannot be driven from a server that holds only an x3d_live_ key. The REST API has no key-authenticated slicing endpoint. The MCP server does: quote_print takes the key, downloads the model from a link and slices it.

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.
shippingSpeedstringoptionaldefault standard
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
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,
  "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
}
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: 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.
total is not what an order will charge
This endpoint adds the requested speed's flat rate unconditionally — $7 standard or $15 express — and never applies a threshold. A real order does: US standard is free at a $49.50 subtotal, international is $30 flat and free at $500, and express is never free at any value. Build your own total from subtotal and the rule for the destination and speed.
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, 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.

Request body (JSON)
generationIdstringrequired
The id of a generation on your own account. Anything else, including a model belonging to someone else, returns 404.
scalePercentintegeroptionaldefault 100
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.
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", 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.
}
200
{
  "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.

Use a scaled slice as it is
Do not multiply the 100% figures by the cube of the scale. Walls and top and bottom skins keep their thickness as a part grows, so one model measured 5.85 times the filament and 3.9 times the print time at 200%, and 17.3 times the filament and 9.9 times the time at 300%, where the cube says 8 and 27. Ask for the size you want and use its numbers unchanged: that is what an order at that size is charged.
StatuserrorWhen
401We 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.
429Too 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.
429Too 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.
429Too 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.
400generationId requiredMissing or non-string generationId.
400material must be one of the material keys.material is not pla, petg, abs, asa, tpu or custom.
400scalePercent must be a whole number from 50 to 300.scalePercent is a fraction, out of range, or not a number.
400At 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.
404Generation not foundNo such generation on your account, or in this signed-out browser's uploads, or it has no model file.
409The reason, with code sliced-scale or project-scaleA size other than 100% for a file you sliced yourself, or for a Bambu Studio or OrcaSlicer project. Both print at their own size.
422The reason, with failed: trueThe 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.
503We 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.
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/GLB models can be slicedThe model is in another format. A GLB is converted to STL on its way to the slicer.
422The slicer's reason, with failed: trueAt 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.
Anything already known about the model at that size, a stored slice or a refusal, is answered as soon as the generation is found: before the rate limit is counted and before the slicer checks below it. Asking again for a size already measured is free, and works 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 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.
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.

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