API quickstart
Get an X3D API key, submit a print order with one multipart POST, poll it until it ships, and read the tracking number back out.
This page goes from nothing to a paid, queued print order. It takes five minutes and one HTTP call. You need an X3D account, a card on it, and a model file — an STL is the easiest thing to test with.
Five steps to a printed part#
- 1Put a card on the account
Sign in and open /profile. In the Developer API key panel, press Add a card on file — Stripe collects the details, we never see them. This is a prerequisite, not a formality: the key cannot be issued without it, because API orders bill automatically with no checkout page in between.
- 2Generate the key
Same panel, press Generate key. The secret is shown once and never again — we store only a SHA-256 hash of it and a display prefix like x3d_live_ab12cd…. Copy it into an environment variable on your server before you close the tab.
bashexport X3D_API_KEY="x3d_live_paste_your_key_here" - 3Submit the order
One multipart/form-data POST carries the file, the address and the print settings. Required: a file (or a modelUrl of a model we already hold for you), recipientName, street1, city, state and zip. Everything else has a default.
bashcurl -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 "street2=Suite 200" \ -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." - 4Read the 201
A successful submit returns 201 with paid: true, the amount charged, and the order code. That code is the only handle you get — store it. It is X3D- followed by six uppercase characters.
json{ "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." } - 5Poll the status
Read the order back with the same key. Poll on a human timescale — a print takes hours, so once every few minutes is plenty, and hourly is fine once it is on a plate.
bashcurl https://x3dstudios.com/api/print/orders/X3D-K7M2QP \ -H "Authorization: Bearer $X3D_API_KEY" - 6Collect the tracking number
tracking stays null until the parcel is handed over. When status becomes SHIPPED it holds the number and the carrier — that is the point at which you can tell your own customer it is on the way.
json{ "code": "X3D-K7M2QP", "status": "SHIPPED", "statusLabel": "Shipped", "paid": true, "fileName": "bracket.stl", "material": "pla", "color": "Black", "quality": "standard", "quantity": 1, "estimatedCost": 9.52, "currency": "USD", "tracking": { "number": "1Z999AA10123456784", "carrier": "UPS" }, "shipTo": { "name": "Ada Lovelace", "city": "Austin", "state": "TX", "country": "US" }, "createdAt": "2026-09-09T15:02:11.417Z", "updatedAt": "2026-09-11T18:40:02.930Z" }
The same submit in three languages#
All three do exactly what the curl above does. In JavaScript and Python, do not set Content-Type yourself — the HTTP client has to write the multipart boundary, and overriding the header is the most common reason a first call comes back as 400 Expected multipart/form-data with a 'file' field.
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 "material=pla" \
-F "color=Black" \
-F "quantity=1"The fields this call used#
/api/print/ordersAPI keySubmit a print order. multipart/form-data. Charges the card on file.
filefilerequired- The model. STL, GLB, .gcode or .gcode.3mf, up to 100MB. Optional only if you send modelUrl instead — a gs:// URI of a model X3D already stores for your account, which saves you downloading and re-uploading something we generated.
recipientNamestringrequired- Who the parcel is addressed to. The alias name is accepted for the same field.
street1stringrequired- First address line. The alias address1 is accepted.
citystringrequired- City.
statestringrequired- State or region. Not validated against a list.
zipstringrequired- Postal code. The alias postalCode is accepted.
countrystringoptionaldefaultUS- Uppercased before checking. Anything outside this list is a 400 that names the country back to you.
USCAGBAUDEFRINNLIENZSEITESMX materialstringoptionaldefaultpla- Lowercased. An unrecognised value is a 400, not a silent fallback.
plapetgabsasatpu qualitystringoptionaldefaultstandard- standard is 0.2mm layers at 1x the rate; premium is 0.1mm at 1.75x.
standardpremium quantityintegeroptionaldefault1- Clamped to 1–50 rather than rejected. 5 or more takes 5% off, 10 or more takes 10%.
colorstringoptionaldefaultDefault- A stock spool name — Black, White, Red, Cyan, Sakura Pink, Sunny Orange, Green, Yellow, Mint Green, Lavender, Silver or Brown. Anything else, including the default, prices at the $0.20/g custom-colour floor.
infillintegeroptionaldefault20- Percent, clamped to 10–100. Ignored for an already-sliced file, which is stored as 100 because the slicer already decided.
notestringoptional- A message to the operator. Silently truncated at 1000 characters.
Nozzle diameter, flow mode and layer height are accepted too. They change how the part is printed and the printHours in the breakdown, but not what it costs — the price is weight × rate × quality factor. Those, and the exact response shape for every branch, are on /docs/api/submit-order.
When the charge fails#
A declined or missing card does not lose the order. You still get 201, the order is stored at PENDING_PAYMENT, and the body carries a way to pay it: paymentUrl is an X3D page for the order, checkoutUrl is the hosted Stripe page. Nothing reaches the farm until one of them is paid.
{
"code": "X3D-P4T8WN",
"estimatedCost": 9.52,
"currency": "USD",
"breakdown": { "print": 2.52, "shipping": 7, "grams": 21, "printHours": 1.04 },
"statusUrl": "https://x3dstudios.com/print/order/X3D-P4T8WN",
"apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-P4T8WN",
"status": "PENDING_PAYMENT",
"paid": false,
"paymentUrl": "https://x3dstudios.com/print/order/X3D-P4T8WN/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."
}Reading the order back#
/api/print/orders/{code}API keyStatus, price and tracking for one order. Only the key that owns it can read it.
The code is matched exactly, so pass it back as it was given — uppercase. A code that does not exist and a code belonging to someone else both return the same 404 Order not found, on purpose. shipTo comes back deliberately trimmed to name, city, state and country: the street, postcode, email and phone are not echoed.
First calls that fail#
| Status | Message | Usually means |
|---|---|---|
| 401 | Invalid or missing API key. Generate one on your X3D profile. | No Authorization header, or the key was rotated since you copied it. |
| 400 | Expected multipart/form-data with a 'file' field. | You sent JSON, or overrode Content-Type and broke the boundary. |
| 400 | Missing required shipping fields: recipientName, city | Exactly what it says, in the order recipientName, street1, city, state, zip. |
| 415 | Unsupported file. Send an STL or GLB model, or an already-sliced .gcode / .gcode.3mf. | An OBJ, a STEP, or a file whose name lost its extension. |
| 422 | We couldn't read a printable mesh from that file. Make sure it's a valid, watertight STL/GLB. | The mesh is broken, empty, or not really the format its name claims. |
| 413 | File too large (max 100MB). | Decimate the mesh, or slice it yourself and send the G-code. |