X3DStudios

Submit a print order

POST a model, an address and print settings to /api/print/orders with your API key: X3D slices it, charges your card on file, and prints it.

This endpoint takes a model file, a shipping address and a few print settings, and returns a paid order that is queued at the farm in Austin. One multipart POST does the whole thing: upload, analysis, quote, charge. There is no separate checkout step and no sandbox — a successful call charges the card on your account.

POST/api/print/ordersAPI key

Create a print order from an uploaded model, and charge the saved card.

Authenticate with Authorization: Bearer x3d_live_… or with x-api-key. The body must be multipart/form-data — JSON is rejected with a 400. See /docs/api/authentication for how the header is read, and /docs/api/keys for how to mint the key.

The model#

Send exactly one of these. A file part wins if you send both.

filefilerequired
The model, as a multipart file part. Required unless you send modelUrl. Accepted extensions: .stl, .glb, .gcode, .3mf (a .gcode.3mf is treated as 3mf). Anything else is a 415. Maximum 100 MB; an empty part is a 400.
modelUrlstringoptional
Alternative to file: the gs:// URI of a model X3D already holds for your account. It must be a managed X3D storage URI and it must belong to the calling account — a URI you do not own returns 403.
Sliced files are priced from the slice
A .gcode or .gcode.3mf carries the filament grams and print time the slicer measured, including purge and tool changes. We bill those numbers rather than estimating from geometry, and the build-volume check is skipped because your slicer already fitted the plate.
materialstringoptionaldefault pla
Case-insensitive. Sets the per-gram rate and the density used to turn volume into weight: PLA $0.12/g, PETG $0.12/g, ABS $0.12/g, TPU $0.13/g, ASA $0.14/g. Anything else is a 400.plapetgabsasatpu
qualitystringoptionaldefault standard
Sets the price factor only: standard is 1x the per-gram rate, premium is 1.75x. It does not change the layer height on this endpoint — that comes from layerHeight, which stays at its own default of 0.2 mm whichever quality you send. Anything else is a 400.standardpremium
quantityintegeroptionaldefault 1
Clamped to 1–50, never rejected: a 0 or a non-number becomes 1, and 200 becomes 50. Five or more units take 5% off, ten or more take 10%.
colorstringoptionaldefault Default
A stock spool name or its hex, matched case-insensitively. Free text is accepted, but anything off the stock list is billed at the $0.20/g custom-colour floor because we have to buy the spool in. Send a stock name even when you do not care — omitting the field stores the literal "Default", which is not a stock colour and prices at the floor.BlackWhiteRedCyanSakura PinkSunny OrangeGreenYellowMint GreenLavenderSilverBrown
infillintegeroptionaldefault 20
Percent. Clamped to 10–100, never rejected. Ignored for an already-sliced file, which is recorded as 100 because the slice already decided.
nozzleDiameternumberoptionaldefault 0.4
Millimetres. Any other value falls back to 0.4 silently — there is no error, so check what came back on the order if it matters.0.20.40.60.8
nozzleFlowstringoptionaldefault standard
high raises the modelled throughput by 1.6x, which shortens the estimated print time. Any value other than "high" is read as standard, silently.standardhigh
layerHeightnumberoptionaldefault 0.2
Millimetres. Clamped, never rejected, to 20–75% of the nozzle diameter — 0.08–0.30 mm on a 0.4 mm nozzle. Out-of-range values are pulled to the nearest end.
notestringoptional
Free text for the operator: orientation, support preferences, anything a human should read before printing. Truncated to 1000 characters without warning.

Recipient and shipping address#

recipientNamestringrequired
Name on the parcel. The alias name is accepted for the same field.
street1stringrequired
Street address. The alias address1 is accepted.
street2stringoptional
Apartment, suite, unit. The alias address2 is accepted. Stored as null when empty.
citystringrequired
City.
statestringrequired
State or province. Not validated against a list.
zipstringrequired
Postal code. The alias postalCode is accepted. Not format-checked.
countrystringoptionaldefault US
Uppercased before the check. Anywhere else returns 400. US postage is $7 flat, free at a $50 subtotal; everywhere else is $30 flat, free at $500.USCAGBAUDEFRINNLIENZSEITESMX
emailstringoptionaldefault your account email
Lowercased. Where the confirmation and stage emails go. Send the end customer's address if you are printing on their behalf.
phonestringoptional
Optional, stored as-is for the carrier. Not validated.
Missing fields are reported once, in a fixed order
The check names every empty required field in one message, always in the order recipientName, street1, city, state, zip — for example "Missing required shipping fields: city, zip".

Request#

curl -X POST https://x3dstudios.com/api/print/orders \
  -H "Authorization: Bearer $X3D_API_KEY" \
  -F "[email protected]" \
  -F "recipientName=Ada Lovelace" \
  -F "street1=1100 Congress Ave" \
  -F "city=Austin" \
  -F "state=TX" \
  -F "zip=78701" \
  -F "country=US" \
  -F "material=pla" \
  -F "color=Black" \
  -F "quality=standard" \
  -F "quantity=1" \
  -F "infill=20" \
  -F "note=Flat face down, no supports on the top surface."

Response#

Every successful submit returns 201, whether or not the card went through. Read status and paid rather than the HTTP code to know which happened.

201 — card charged, order queued
{
  "code": "X3D-K7M2QP",
  "estimatedCost": 9.52,
  "currency": "USD",
  "breakdown": { "print": 2.52, "shipping": 7, "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-K7M2QP",
  "status": "RECEIVED",
  "paid": true,
  "charged": 9.52,
  "paymentUrl": null,
  "message": "Order received and your card was charged $9.52. It's queued at the farm — track it at statusUrl."
}
201 — charge failed, order held for payment
{
  "code": "X3D-K7M2QP",
  "estimatedCost": 9.52,
  "currency": "USD",
  "breakdown": { "print": 2.52, "shipping": 7, "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-K7M2QP",
  "status": "PENDING_PAYMENT",
  "paid": false,
  "paymentUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP/pay",
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_…",
  "message": "Auto-charge failed (No card on file). Update your card on file, or pay via paymentUrl."
}
Response fields
codestringoptional
The public order code: X3D- followed by six uppercase characters from an alphabet with no 0, 1, I, L or O. This is the only handle you get — store it.
estimatedCostnumberoptional
Print plus postage, in USD, rounded to the cent. This is the amount charged.
currencystringoptional
Always "USD".
breakdown.printnumberoptional
The print subtotal before postage: weight x rate x quality factor x quantity, with the bulk discount and the $1.00-per-part minimum applied.
breakdown.shippingnumberoptional
Postage. 7 or 0 domestically, 30 or 0 internationally.
breakdown.gramsnumberoptional
Billable weight, rounded up to the whole gram.
breakdown.printHoursnumberoptional
Estimated machine hours. Informational — the only thing that adds cost on top of the per-gram rate is an already-sliced file whose measured time far exceeds what its weight implies.
statusUrlstringoptional
Human-readable order page. It needs no login, so it is safe to hand to your own customer.
apiStatusUrlstringoptional
The URL to poll with your key. See /docs/api/order-status.
statusstringoptional
RECEIVED when the card was charged, PENDING_PAYMENT when it was not.RECEIVEDPENDING_PAYMENT
paidbooleanoptional
Whether money has actually been taken.
chargednumberoptional
Present only on the paid branch: the amount taken from the card.
paymentUrlstring | nulloptional
X3D payment page for an unpaid order. null when the order is already paid, and null if Stripe returned no link.
checkoutUrlstring | nulloptional
Raw Stripe Checkout URL. Present only on the unpaid branch; absent entirely when the card was charged.
messagestringoptional
One sentence you can log or show. On the unpaid branch it quotes the decline reason verbatim.

Errors#

Every failure is a JSON object with a single error key. Validation stops at the first problem, in this order: file, shipping fields, country, material, quality, file extension, storage, analysis, minimum total.

StatuserrorCause
401Invalid or missing API key. Generate one on your X3D profile.No credential, or one we do not recognise.
400Expected multipart/form-data with a 'file' field.The body did not parse as multipart. Usually a JSON body.
400The file is empty.A file part of zero bytes.
413File too large (max 100MB).The file part is over 100 MB.
400No file provided.Neither a file part nor modelUrl.
400modelUrl must be a model stored by X3D.modelUrl is not a managed X3D storage URI.
403That model does not belong to this account.The modelUrl belongs to another account.
404Could not read that model.The stored object could not be fetched.
400Missing required shipping fields: …One or more of recipientName, street1, city, state, zip is empty.
400We don't currently ship to XX.The country is not US and not on the international list.
400Invalid material. One of: pla, petg, abs, asa, tpuUnrecognised material.
400Invalid quality. One of: standard, premiumUnrecognised quality.
415Unsupported file. Send an STL or GLB model, or an already-sliced .gcode / .gcode.3mf.The filename extension is not one we print.
502Could not store the file. Try again.The upload to storage failed. Retry.
422We couldn't read a printable mesh from that file. …The model parsed but is not printable. The message names the specific problem.
422This model is 400×80×20mm — larger than the 340×320×340mm build volume. …A mesh that does not fit the plate. Scale it down or split it.
400Order total is below the $0.50 minimum.Stripe cannot take a charge under 50 cents. In practice unreachable domestically, where postage alone is $7.
Exact status codes and error strings returned by POST /api/print/orders.
The order exists before the charge
The row is written at PENDING_PAYMENT, then the card is charged. If the charge fails you have a real order that can still be paid — do not retry the submit, or you will create a second one. Retry only on a 5xx, where no code came back.