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. A .gcode.3mf whose plate uses five or more filaments also pays the $25 second-AMS fee, once per order, because the farm has to chain a second unit on for it. The count comes from the plate's own list of filaments it uses. A plain .gcode lists every filament its project defined, used or not, so it is priced as one colour.
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, ASA $0.14/g, TPU $0.25/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. Omitting the field stores the literal "Default", which means our choice of a stock spool and prices at the material rate.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 standard, free at a $49.50 subtotal; everywhere else is $30 flat, free at $500 and standard service only.USAEATAUBECACHDEDKESFIFRGBIEINITJPMXNLNONZPTSESG
shippingSpeedstringoptionaldefault standard
standard is $7 and 4–6 business days in transit; express is $15 and 2–3. Express is US only and asking for it on a country other than US returns 400 rather than quietly shipping standard — this is the one order path that knows the destination before it prices. Any value other than standard or express is a 400 too. The service is echoed back in breakdown.shippingSpeed. It changes the postage only: the print itself is queued as Normal print, because Priority print is not sold through the API.standardexpress
emailstringoptionaldefault your account email
One to five addresses that get the order emails: the confirmation, printed, shipped with tracking, and delivered. Repeat the field for each address, or separate them with commas or semicolons in one field. The first address is the main contact; every address gets every email. Lowercased, and repeats are dropped. Send the end customer's address if you are printing on their behalf.
phonestringoptional
Optional, stored as-is for the carrier. Not validated.

Order emails to more than one person#

Send email once for each person who should hear about the order, up to five in total. The first address is the main contact. All of them get the same emails at the same moments: the confirmation when the card is charged, then printed, shipped with the tracking number, and delivered. A field like [email protected], [email protected] works the same as two separate fields. An address that is not valid, or more than five addresses, is a 400 that names the problem, and nothing is charged.

curl -X POST https://x3dstudios.com/api/print/orders \
  -H "Authorization: Bearer $X3D_API_KEY" \
  -F "[email protected]" \
  -F "recipientName=Ada Lovelace" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "street1=1100 Congress Ave" \
  -F "city=Austin" -F "state=TX" -F "zip=78701" -F "country=US" \
  -F "material=pla" -F "color=Black"
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, "shippingSpeed": "standard", "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-K7M2QP",
  "email": "[email protected]",
  "notifyEmails": ["[email protected]"],
  "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, "shippingSpeed": "standard", "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-K7M2QP",
  "email": "[email protected]",
  "notifyEmails": [],
  "status": "PENDING_PAYMENT",
  "paid": false,
  "chargeOutcome": "not-charged",
  "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) and nothing was taken. 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, plus the second-AMS fee when it applies, 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 on domestic standard, always 15 on domestic express, 30 or 0 internationally.
breakdown.shippingSpeedstringoptional
Which service that postage bought — "standard" or "express". Echoed back because the amount alone cannot say: $15 is express, but it is also two standard parcels.
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.
breakdown.secondAmsFeenumberoptional
Present only when it is charged: 25, for a sliced .gcode.3mf whose plate uses five or more filaments. Once per order, whatever the quantity, and included in estimatedCost.
breakdown.coloursnumberoptional
Present only with secondAmsFee: how many filaments the plate uses.
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.
emailstringoptional
The order's main contact: the first address you sent, or your account email when you sent none.
notifyEmailsstring[]optional
The other addresses that get every order email, in the order you sent them. An empty array when there is only the main contact.
statusstringoptional
RECEIVED when the card was charged, PENDING_PAYMENT when it was not or the charge could not be confirmed.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.
chargeOutcomestringoptional
Present only on the unpaid branch. not-charged means the card was provably not charged: a decline, or no card on file. unknown means the charge may have gone through and only its confirmation was lost; read the order back before anyone pays again.not-chargedunknown
messagestringoptional
One sentence you can log or show. On the unpaid branch it quotes the decline reason verbatim, and when chargeOutcome is unknown it says the charge may have gone through.

Errors#

Every failure is a JSON object with a single error key. Validation stops at the first problem, in this order: file, email addresses, shipping fields, country, shipping speed, 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.
400"…" is not an email address.One of the addresses in email is not a valid address. The message quotes it.
400An order takes up to 5 email addresses; 6 were given.More than five different addresses across all email fields.
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 shippingSpeed. One of: standard, express.shippingSpeed is set to anything else.
400Express shipping is US only — it is a domestic carrier commitment. Omit shippingSpeed to ship standard to XX.shippingSpeed=express on an order whose country is not US.
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.

Questions

What happens if the card on file is declined?

You still get 201 and an order code, but with status PENDING_PAYMENT and paid: false. The response carries checkoutUrl (a Stripe Checkout link) and paymentUrl (the X3D payment page for that order), and chargeOutcome says whether the card was provably not charged ("not-charged") or the charge could not be confirmed ("unknown"). Nothing reaches the farm until the order is paid.

Can I order a model X3D already stores for me?

Yes. Send modelUrl instead of file, with the gs:// URI of a model generated on your own account. We check ownership server-side and refuse a URI that belongs to somebody else, so you never have to download and re-upload a file we already hold.

Can more than one person get the order emails?

Yes. The email field takes up to five addresses. Repeat the field, or put them in one field separated by commas. The first address is the main contact, and every address gets the confirmation, the printed and shipped emails with tracking, and the delivered email.

Do I have to slice the file first?

No. Send an STL or GLB and we slice it. If you send an already-sliced .gcode or .gcode.3mf, we price it from the filament grams and print time recorded in the file, and the infill field is ignored because the slice already fixed it.

Can I pay for Priority print through the API?

Not yet. Priority print is a website checkout option. There is no field for it on this endpoint, and every API order is queued as Normal print. shippingSpeed=express buys faster postage, not a faster print.