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, nothing charged. 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
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 | 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 CA, GB, AU, DE, FR, IN, NL, IE, NZ, SE, IT, ES, MX. |
| 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.
- 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.