Errors
Every status code the X3D API returns, the exact message that comes with it, and which failures are safe to retry — including the 201 that is not a success.
Every failure from the X3D API is a JSON object with one key. There are no error codes to map, no nested error objects, and no envelope around successful responses. This page lists the exact string each endpoint returns with each status, and says which failures are worth retrying.
The error shape#
{ "error": "Invalid material. One of: pla, petg, abs, asa, tpu" }The message is written for a human reading a log, not for a switch statement. Match on the HTTP status, not on the text — the strings can be reworded, and two different causes sometimes share one status.
One response adds a second key. POST /api/account/api-key returns 402 with a machine-readable code when the account has no card on file. It is the only endpoint in this API that does that.
{
"error": "Add a card on file first — API print orders are billed automatically to your saved card.",
"code": "CARD_REQUIRED"
}const res = await fetch("https://x3dstudios.com/api/print/orders", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.X3D_API_KEY}` },
body: form,
});
const data = await res.json();
if (!res.ok) {
// Every failure is { error: "<message>" }.
throw new Error(`X3D ${res.status}: ${data.error}`);
}
if (!data.paid) {
// 201, real order code, not paid. It will sit at PENDING_PAYMENT until
// someone pays it, and the farm will not queue an unpaid order.
console.warn(data.message); // names the decline reason
if (data.chargeOutcome === "unknown") {
// The charge may have gone through. Read the order back before paying.
console.warn(data.apiStatusUrl);
} else {
console.warn(data.paymentUrl); // hosted page to finish paying
}
}Submitting an order#
/api/print/ordersAPI keySubmit a file and a shipping address. Charges the card on file.
| Status | error | What caused it |
|---|---|---|
| 401 | Invalid or missing API key. Generate one on your X3D profile. | No credential, or one that does not resolve to an account. Also returned for a key that has been rotated or revoked. |
| 400 | Expected multipart/form-data with a 'file' field. | The body could not be parsed as a form. Usually a JSON body, or a Content-Type header you set by hand that overrode the multipart boundary. |
| 400 | The file is empty. | A file part was present with zero bytes. |
| 413 | File too large (max 100MB). | The uploaded file is over 100 MB. |
| 400 | No file provided. | Neither a file part nor a modelUrl was sent. Sending a filename as a plain string, not a file, lands here too. |
| 400 | modelUrl must be a model stored by X3D. | modelUrl was not one of our own storage URIs. You cannot point this field at an arbitrary URL. |
| 403 | That model does not belong to this account. | The modelUrl is ours but was generated by a different account. |
| 404 | Could not read that model. | The modelUrl resolved but the object could not be fetched. |
| 400 | "…" is not an email address. | One of the addresses sent in email is not a valid address. email takes up to five, repeated or comma separated, and the message quotes the one that failed. |
| 400 | An order takes up to 5 email addresses; 6 were given. | More than five different addresses across every email field. Repeats are dropped before counting. |
| 400 | Missing required shipping fields: … | One or more of recipientName, street1, city, state, zip is blank. The missing names are listed in that order. |
| 400 | We don't currently ship to XX. | The country is neither US nor one of the 23 international destinations on the shipping page. |
| 400 | Invalid shippingSpeed. One of: standard, express. | shippingSpeed was set to something other than those two. Leave it out for standard. |
| 400 | Express shipping is US only — it is a domestic carrier commitment. Omit shippingSpeed to ship standard to XX. | shippingSpeed=express on an order going outside the US. Express is refused rather than quietly downgraded. |
| 400 | Invalid material. One of: pla, petg, abs, asa, tpu | material was set to something outside that list. It is lowercased before the check, so PLA is fine. |
| 400 | Invalid quality. One of: standard, premium | quality was set to anything other than those two. |
| 415 | Unsupported file. Send an STL or GLB model, or an already-sliced .gcode / .gcode.3mf. | The filename extension is not stl, glb, gcode or 3mf. OBJ is rejected here. |
| 502 | Could not store the file. Try again. | Our object storage refused the upload. Nothing was persisted. |
| 422 | (see the analysis messages below) | The file was stored but could not be read as a printable model. |
| 400 | Order total is below the $0.50 minimum. | The quote came out under 50 cents, which is the floor our payment processor will take. |
The 422 analysis messages#
A 422 means the upload succeeded and the file was rejected on inspection. There are four strings you can actually receive here, and each points at a different fix.
| error | Fix |
|---|---|
| We couldn't read a printable model from that file. | The parser threw. The file is corrupt, or it is not really the format its extension claims. |
| We couldn't read a printable mesh from that file. Make sure it's a valid, watertight STL/GLB. | The mesh parsed but has no usable triangles or a zero-size bounding box. Re-export it. |
| We couldn't read the print footprint/filament from that sliced file. Re-export it from Bambu Studio / OrcaSlicer. | A .gcode or .gcode.3mf whose header carries no filament weight or plate footprint. Slice it again with a Bambu profile. |
| This model is 400×90×20mm — larger than the 340×320×340mm build volume. Scale it down or split it. | The real dimensions are interpolated into the message. Meshes only — an already-sliced file skips this check, because its slicer already fitted it to a plate. |
Which check fires first#
Validation short-circuits on the first failure, so a request with three problems reports one. If you are debugging a 400 that seems to name the wrong field, this is the order the route works in.
- The credential.
- A readable multipart body.
- A file part, or a modelUrl you own.
- The five required shipping fields, then the country, then shippingSpeed.
- material, then quality.
- The file extension.
- Storing the file, then analysing it.
- The $0.50 total floor.
- Writing the order, then charging the card.
Reading an order#
/api/print/orders/{code}API keyStatus, price and tracking for one order you own.
| Status | error | What caused it |
|---|---|---|
| 401 | Invalid or missing API key. | A shorter string than the submit endpoint's 401, and a different one — do not match on the text. Extension tokens are rejected here even though they are accepted on submit. |
| 404 | Order not found. | No such code, or the order belongs to another account. The two are deliberately indistinguishable. |
Managing your key#
The three key-management methods live on /api/account/api-key and authenticate with a signed-in browser session, not with a key. They return the same one-key envelope.
| Status | error | Applies to |
|---|---|---|
| 401 | Unauthorized | GET, POST and DELETE, when there is no signed-in session. |
| 402 | Add a card on file first — API print orders are billed automatically to your saved card. | POST only. Carries code: "CARD_REQUIRED". Add a card at /profile and try again. |
What to retry#
| Status | Retry? | Why |
|---|---|---|
| 400, 403, 413, 415, 422 | No | Deterministic. The same request fails the same way forever. Fix the input. |
| 401 | No | The key is wrong, rotated or revoked. Retrying with the same credential cannot succeed. |
| 404 | No, with one exception | On the status endpoint it is final. On submit it means a modelUrl we could not fetch, which is worth one retry if the model was created seconds earlier. |
| 502 | Yes | Storage refused the file and nothing was persisted — no order, no charge. Back off a few seconds and send it again. |
| 5xx or a dropped connection | Carefully | The order may or may not exist. See the warning below before you send anything twice. |
| 201 with paid: false | No | The order exists. Retrying makes a duplicate. Fix the card and pay the order you already have. |
One status you will not see from these endpoints is 429. Nothing in the application rate-limits the print API today, so there is no backoff to implement here — see /docs/api/rate-limits for what that does and does not promise.
Which endpoints cap you per hour, which have no limit at all, and what a 429 carries.
Every field the submit endpoint accepts, and what each one defaults to.
When the card is charged, what the charge covers, and what a decline leaves behind.
Key format, both accepted headers, rotation and revocation.
Questions
Does a 201 from the X3D API mean the order was paid?
No. A failed card charge still returns 201 with an order code, because the order row is written before the charge is attempted. Read the paid field: true means the card was charged, false means the order is sitting at PENDING_PAYMENT and someone has to open paymentUrl.
Is it safe to retry a print order submission that timed out?
No. There is no idempotency key on POST /api/print/orders, so a blind retry can create a second order and a second charge. Check whether the first attempt landed before sending anything again: the REST API has no list endpoint, but the MCP tool list_my_orders takes the same key and returns your 20 most recent orders, and /orders lists them when you are signed in. If neither shows it, mail [email protected] with the file name and address.
Why does the status endpoint return 404 for an order I know exists?
Two reasons produce the same 404. The code is matched exactly and is case-sensitive, so x3d-k7m2qp will not find X3D-K7M2QP. And an order belonging to another account returns 404 rather than 403, so ownership is never leaked.
Related guides
- Build 3D Printing Quote Software: Price a Print in CodeBuild 3D printing quote software in about fifteen lines: mesh volume to billable grams times the per-gram rate, or POST a bounding box to the quote API.
- 3D Printing API: Manufacturing as CodeA 3D printing API turns manufacturing into code: POST a model and an address, get a real part shipped. Who needs a print on demand API and what it costs.