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, TPU $0.13/g, ASA $0.14/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. 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 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, free at a $50 subtotal; everywhere else is $30 flat, free at $500.
USCAGBAUDEFRINNLIENZSEITESMX emailstringoptionaldefaultyour 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.
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, "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."
}{
"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."
}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.
| 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 | 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 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. |