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. Order emails go to your account email unless you send email, which takes up to five addresses: repeat the field for each person who should hear about the order.

    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, "shippingSpeed": "standard", "grams": 21, "printHours": 1.04 },
      "statusUrl": "https://x3dstudios.com/print/order/X3D-K7M2QP",
      "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-K7M2QP",
      "email": "[email protected]",
      "notifyEmails": [],
      "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" },
      "email": "[email protected]",
      "notifyEmails": [],
      "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.USAEATAUBECACHDEDKESFIFRGBIEINITJPMXNLNONZPTSESG
materialstringoptionaldefault pla
Lowercased. An unrecognised value is a 400, not a silent fallback.plapetgabsasatpu
qualitystringoptionaldefault standard
The price factor: standard is 1x the rate, premium is 1.75x. It does not set the layer height on this endpoint; layerHeight does, and it defaults to 0.2mm whichever quality you send.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 prices at the $0.20/g custom-colour floor. Left out, it is stored as Default: our choice of a stock spool, at the material rate.
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.

Postage, and what the API does not sell
Postage is $7 standard in the US (free from a $49.50 print subtotal), $15 with shippingSpeed=express, or $30 to the international list. Priority print, the website's option to print an order ahead of the queue, is not available through the API yet: every API order is queued as Normal print.

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. Read chargeOutcome first: "not-charged" means nothing was taken, while "unknown" means the charge may have gone through with its confirmation lost, so read the order back before anyone pays again.

201 with paid: false
{
  "code": "X3D-P4T8WN",
  "estimatedCost": 9.52,
  "currency": "USD",
  "breakdown": { "print": 2.52, "shipping": 7, "shippingSpeed": "standard", "grams": 21, "printHours": 1.04 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-P4T8WN",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-P4T8WN",
  "email": "[email protected]",
  "notifyEmails": [],
  "status": "PENDING_PAYMENT",
  "paid": false,
  "chargeOutcome": "not-charged",
  "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) and nothing was taken. 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, in 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 and phone are not echoed. email and notifyEmails list who gets the order emails.

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.

Questions

How much does the smallest possible test order cost?

About $8 in the US. Printing has a $1.00 minimum per part, and US postage is $7 flat below a $49.50 subtotal. A one-gram part and an eight-gram part therefore cost the same: 8g of PLA at $0.12/g is $0.96, still under the minimum, and only at about 9g does the per-gram rate start to bite.

Do I have to slice the file first?

No. Send an STL or GLB and we slice it. If you would rather control the print exactly, send a .gcode or .gcode.3mf you exported from Bambu Studio or OrcaSlicer — we price it from the filament grams and print time in the file and print it as exported.

Why did my order come back with status PENDING_PAYMENT?

The order was created but the card on file could not be charged. The response message says why, and carries checkoutUrl and paymentUrl so the order can still be paid. Nothing goes to the farm until it is.