X3DStudios

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#

Every non-2xx response
{ "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.

The only error with a code field
{
  "error": "Add a card on file first — API print orders are billed automatically to your saved card.",
  "code": "CARD_REQUIRED"
}
201 does not mean paid
The order row is written before the card is charged, so a declined card still returns 201 with a real order code. The response then carries paid: false, status PENDING_PAYMENT, and a message naming the decline reason. Check the paid field on every 201 — treating 201 as done means silently dropping orders that nobody ever paid for.
Handling both halves of a 201
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#

POST/api/print/ordersAPI key

Submit a file and a shipping address. Charges the card on file.

StatuserrorWhat caused it
401Invalid 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.
400Expected 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.
400The file is empty.A file part was present with zero bytes.
413File too large (max 100MB).The uploaded file is over 100 MB.
400No file provided.Neither a file part nor a modelUrl was sent. Sending a filename as a plain string, not a file, lands here too.
400modelUrl 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.
403That model does not belong to this account.The modelUrl is ours but was generated by a different account.
404Could not read that model.The modelUrl resolved but the object could not be fetched.
400Missing required shipping fields: …One or more of recipientName, street1, city, state, zip is blank. The missing names are listed in that order.
400We 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.
400Invalid material. One of: pla, petg, abs, asa, tpumaterial was set to something outside that list. It is lowercased before the check, so PLA is fine.
400Invalid quality. One of: standard, premiumquality was set to anything other than those two.
415Unsupported 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.
502Could 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.
400Order total is below the $0.50 minimum.The quote came out under 50 cents, which is the floor our payment processor will take.
Message strings are reproduced exactly as the route returns them.

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.

errorFix
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.

  1. The credential.
  2. A readable multipart body.
  3. A file part, or a modelUrl you own.
  4. The five required shipping fields, then the country.
  5. material, then quality.
  6. The file extension.
  7. Storing the file, then analysing it.
  8. The $0.50 total floor.
  9. Writing the order, then charging the card.
Some bad input never errors
quantity, infill, layerHeight, nozzleDiameter, nozzleFlow and note are coerced rather than rejected. quantity 900 becomes 50, quantity 0 becomes 1, a 0.5 mm nozzle silently becomes 0.4, and a note over 1000 characters is truncated. You get a 201 and a charge for what the coercion produced, so validate these yourself before sending.

Reading an order#

GET/api/print/orders/{code}API key

Status, price and tracking for one order you own.

StatuserrorWhat caused it
401Invalid 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.
404Order not found.No such code, or the order belongs to another account. The two are deliberately indistinguishable.
Codes are matched exactly
Order codes are X3D- plus six uppercase characters from the alphabet 23456789ABCDEFGHJKMNPQRSTUVWXYZ — no 0, 1, I, L or O. The lookup does no normalising, so a lowercased or whitespace-padded code returns 404. Store the code exactly as it came back.

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.

StatuserrorApplies to
401UnauthorizedGET, POST and DELETE, when there is no signed-in session.
402Add 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#

StatusRetry?Why
400, 403, 413, 415, 422NoDeterministic. The same request fails the same way forever. Fix the input.
401NoThe key is wrong, rotated or revoked. Retrying with the same credential cannot succeed.
404No, with one exceptionOn 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.
502YesStorage refused the file and nothing was persisted — no order, no charge. Back off a few seconds and send it again.
5xx or a dropped connectionCarefullyThe order may or may not exist. See the warning below before you send anything twice.
201 with paid: falseNoThe order exists. Retrying makes a duplicate. Fix the card and pay the order you already have.
There is no idempotency key
The API accepts no Idempotency-Key header, and there is no endpoint that lists your orders, so after a timeout you cannot ask whether the first attempt landed. Two identical submissions are two orders and two charges. If a submit call dies mid-flight, mail [email protected] with the file name and address before resending.

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.