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.
/api/print/ordersAPI keyCreate 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.
Print configuration#
materialstringoptionaldefaultpla- 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 qualitystringoptionaldefaultstandard- 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 quantityintegeroptionaldefault1- 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%.
colorstringoptionaldefaultDefault- 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 infillintegeroptionaldefault20- Percent. Clamped to 10–100, never rejected. Ignored for an already-sliced file, which is recorded as 100 because the slice already decided.
nozzleDiameternumberoptionaldefault0.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 nozzleFlowstringoptionaldefaultstandard- 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 layerHeightnumberoptionaldefault0.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.
countrystringoptionaldefaultUS- 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 shippingSpeedstringoptionaldefaultstandard- 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 emailstringoptionaldefaultyour 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"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.
{
"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."
}{
"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."
}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.
| Status | error | Cause |
|---|---|---|
| 401 | Invalid or missing API key. Generate one on your X3D profile. | No credential, or one we do not recognise. |
| 400 | Expected multipart/form-data with a 'file' field. | The body did not parse as multipart. Usually a JSON body. |
| 400 | The file is empty. | A file part of zero bytes. |
| 413 | File too large (max 100MB). | The file part is over 100 MB. |
| 400 | No file provided. | Neither a file part nor modelUrl. |
| 400 | modelUrl must be a model stored by X3D. | modelUrl is not a managed X3D storage URI. |
| 403 | That model does not belong to this account. | The modelUrl belongs to another account. |
| 404 | Could 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. |
| 400 | An order takes up to 5 email addresses; 6 were given. | More than five different addresses across all email fields. |
| 400 | Missing required shipping fields: … | One or more of recipientName, street1, city, state, zip is empty. |
| 400 | We don't currently ship to XX. | The country is not US and not on the international list. |
| 400 | Invalid shippingSpeed. One of: standard, express. | shippingSpeed is set to anything else. |
| 400 | Express 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. |
| 400 | Invalid material. One of: pla, petg, abs, asa, tpu | Unrecognised material. |
| 400 | Invalid quality. One of: standard, premium | Unrecognised quality. |
| 415 | Unsupported file. Send an STL or GLB model, or an already-sliced .gcode / .gcode.3mf. | The filename extension is not one we print. |
| 502 | Could not store the file. Try again. | The upload to storage failed. Retry. |
| 422 | We couldn't read a printable mesh from that file. … | The model parsed but is not printable. The message names the specific problem. |
| 422 | This 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. |
| 400 | Order total is below the $0.50 minimum. | Stripe cannot take a charge under 50 cents. In practice unreachable domestically, where postage alone is $7. |
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.