X3DStudios

API quickstart

Get an X3D API key, submit a print order with one multipart POST, poll it until it ships, and read the tracking number back out.

This page goes from nothing to a paid, queued print order. It takes five minutes and one HTTP call. You need an X3D account, a card on it, and a model file — an STL is the easiest thing to test with.

This spends real money
There is no sandbox. POST /api/print/orders charges the card on your account the moment it succeeds, and the job appears on an operator's screen. The cheapest possible US order is $8.00: the $1.00 per-part minimum plus $7 postage.

Five steps to a printed part#

  1. 1
    Put a card on the account

    Sign in and open /profile. In the Developer API key panel, press Add a card on file — Stripe collects the details, we never see them. This is a prerequisite, not a formality: the key cannot be issued without it, because API orders bill automatically with no checkout page in between.

  2. 2
    Generate the key

    Same panel, press Generate key. The secret is shown once and never again — we store only a SHA-256 hash of it and a display prefix like x3d_live_ab12cd…. Copy it into an environment variable on your server before you close the tab.

    bash
    export X3D_API_KEY="x3d_live_paste_your_key_here"
  3. 3
    Submit the order

    One multipart/form-data POST carries the file, the address and the print settings. Required: a file (or a modelUrl of a model we already hold for you), recipientName, street1, city, state and zip. Everything else has a default.

    bash
    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 "street2=Suite 200" \
      -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."
  4. 4
    Read the 201

    A successful submit returns 201 with paid: true, the amount charged, and the order code. That code is the only handle you get — store it. It is X3D- followed by six uppercase characters.

    json
    {
      "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."
    }
  5. 5
    Poll the status

    Read the order back with the same key. Poll on a human timescale — a print takes hours, so once every few minutes is plenty, and hourly is fine once it is on a plate.

    bash
    curl https://x3dstudios.com/api/print/orders/X3D-K7M2QP \
      -H "Authorization: Bearer $X3D_API_KEY"
  6. 6
    Collect the tracking number

    tracking stays null until the parcel is handed over. When status becomes SHIPPED it holds the number and the carrier — that is the point at which you can tell your own customer it is on the way.

    json
    {
      "code": "X3D-K7M2QP",
      "status": "SHIPPED",
      "statusLabel": "Shipped",
      "paid": true,
      "fileName": "bracket.stl",
      "material": "pla",
      "color": "Black",
      "quality": "standard",
      "quantity": 1,
      "estimatedCost": 9.52,
      "currency": "USD",
      "tracking": { "number": "1Z999AA10123456784", "carrier": "UPS" },
      "shipTo": { "name": "Ada Lovelace", "city": "Austin", "state": "TX", "country": "US" },
      "createdAt": "2026-09-09T15:02:11.417Z",
      "updatedAt": "2026-09-11T18:40:02.930Z"
    }

The same submit in three languages#

All three do exactly what the curl above does. In JavaScript and Python, do not set Content-Type yourself — the HTTP client has to write the multipart boundary, and overriding the header is the most common reason a first call comes back as 400 Expected multipart/form-data with a 'file' field.

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 "material=pla" \
  -F "color=Black" \
  -F "quantity=1"

The fields this call used#

POST/api/print/ordersAPI key

Submit a print order. multipart/form-data. Charges the card on file.

The minimum set
filefilerequired
The model. STL, GLB, .gcode or .gcode.3mf, up to 100MB. Optional only if you send modelUrl instead — a gs:// URI of a model X3D already stores for your account, which saves you downloading and re-uploading something we generated.
recipientNamestringrequired
Who the parcel is addressed to. The alias name is accepted for the same field.
street1stringrequired
First address line. The alias address1 is accepted.
citystringrequired
City.
statestringrequired
State or region. Not validated against a list.
zipstringrequired
Postal code. The alias postalCode is accepted.
countrystringoptionaldefault US
Uppercased before checking. Anything outside this list is a 400 that names the country back to you.USCAGBAUDEFRINNLIENZSEITESMX
materialstringoptionaldefault pla
Lowercased. An unrecognised value is a 400, not a silent fallback.plapetgabsasatpu
qualitystringoptionaldefault standard
standard is 0.2mm layers at 1x the rate; premium is 0.1mm at 1.75x.standardpremium
quantityintegeroptionaldefault 1
Clamped to 1–50 rather than rejected. 5 or more takes 5% off, 10 or more takes 10%.
colorstringoptionaldefault Default
A stock spool name — Black, White, Red, Cyan, Sakura Pink, Sunny Orange, Green, Yellow, Mint Green, Lavender, Silver or Brown. Anything else, including the default, prices at the $0.20/g custom-colour floor.
infillintegeroptionaldefault 20
Percent, clamped to 10–100. Ignored for an already-sliced file, which is stored as 100 because the slicer already decided.
notestringoptional
A message to the operator. Silently truncated at 1000 characters.

Nozzle diameter, flow mode and layer height are accepted too. They change how the part is printed and the printHours in the breakdown, but not what it costs — the price is weight × rate × quality factor. Those, and the exact response shape for every branch, are on /docs/api/submit-order.

Always send color
Omitting it stores the colour as "Default", which is not a stock spool name, so the whole order prices at $0.20/g instead of $0.12/g for PLA. On a 200g part that is a $16 difference for a field you left blank.

When the charge fails#

A declined or missing card does not lose the order. You still get 201, the order is stored at PENDING_PAYMENT, and the body carries a way to pay it: paymentUrl is an X3D page for the order, checkoutUrl is the hosted Stripe page. Nothing reaches the farm until one of them is paid.

201 with paid: false
{
  "code": "X3D-P4T8WN",
  "estimatedCost": 9.52,
  "currency": "USD",
  "breakdown": { "print": 2.52, "shipping": 7, "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-P4T8WN",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-P4T8WN",
  "status": "PENDING_PAYMENT",
  "paid": false,
  "paymentUrl": "https://x3dstudios.com/print/order/X3D-P4T8WN/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."
}
Check paid, not the status code
Both outcomes are 201. If your integration treats 2xx as success and moves on, a declined card looks identical to a printed part. Branch on the paid field.

Reading the order back#

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

Status, price and tracking for one order. Only the key that owns it can read it.

The code is matched exactly, so pass it back as it was given — uppercase. A code that does not exist and a code belonging to someone else both return the same 404 Order not found, on purpose. shipTo comes back deliberately trimmed to name, city, state and country: the street, postcode, email and phone are not echoed.

First calls that fail#

StatusMessageUsually means
401Invalid or missing API key. Generate one on your X3D profile.No Authorization header, or the key was rotated since you copied it.
400Expected multipart/form-data with a 'file' field.You sent JSON, or overrode Content-Type and broke the boundary.
400Missing required shipping fields: recipientName, cityExactly what it says, in the order recipientName, street1, city, state, zip.
415Unsupported file. Send an STL or GLB model, or an already-sliced .gcode / .gcode.3mf.An OBJ, a STEP, or a file whose name lost its extension.
422We couldn't read a printable mesh from that file. Make sure it's a valid, watertight STL/GLB.The mesh is broken, empty, or not really the format its name claims.
413File too large (max 100MB).Decimate the mesh, or slice it yourself and send the G-code.
The full list, with every status the route can return, is on /docs/api/errors.