X3DStudios

Add 3D Printing to Your App With One POST Request

X3D Studios··9 min

Adding 3D printing to your app takes one multipart POST to https://x3dstudios.com/api/print/orders carrying a model file, a shipping address and a Bearer key. The response comes back with an order code, the exact amount charged to your card on file, and a URL you can poll for status and tracking. There is no quote request, no sales thread and no separate payment step — the part is priced, paid for and queued at the farm in Austin inside that single round trip.

What One POST Request Actually Buys

Ordering a manufactured part normally means email. A print farm can skip that because nothing in the pipeline needs a human decision: price is a function of weight and material, scheduling is software packing jobs onto idle machines, and the file either passes validation or it does not. The wider argument for treating manufacturing as an HTTP call is in /blog/3d-printing-api-manufacturing-as-code. This post is the narrow version: the request, the fields, the response, and the errors worth handling.

Five-step API flow: your app posts the file, address and Bearer key; the file format and 340 by 320 by 340 mm plate fit are validated; a sliced file is measured and a mesh priced from geometry; the card on file is charged and a 201 returns an order code; the status endpoint returns carrier and tracking
A 201 comes back already paid. Everything after that is a GET.

Three Things Before the First Request

  1. An X3D account with a card on file. Key generation refuses without one, with a CARD_REQUIRED response — API orders bill automatically, so there is nowhere else for the money to come from.
  2. A key. Generate it on your profile page; the plaintext is shown exactly once and only a SHA-256 hash is stored. Keys look like x3d_live_… and are sent as Authorization: Bearer, or as x-api-key if your HTTP client makes that easier.
  3. A file in a format the farm accepts: STL, GLB, .gcode or a Bambu .gcode.3mf, up to 100 MB. OBJ is not accepted, so convert first if that is what your pipeline produces.
⚠️Rotating a key invalidates the old one immediately — there is no grace window and no second active key. Deploy the new value before you rotate, not after. If you are unsure which format to send, /blog/what-file-do-i-need-to-3d-print covers the trade-offs.

The Request

curl -X POST https://x3dstudios.com/api/print/orders \
  -H "Authorization: Bearer $X3D_API_KEY" \
  -F "[email protected]" \
  -F "material=pla" \
  -F "color=Matte Black" \
  -F "quality=standard" \
  -F "quantity=1" \
  -F "recipientName=Ada Lovelace" \
  -F "[email protected]" \
  -F "street1=1 Analytical Way" \
  -F "city=Austin" -F "state=TX" -F "zip=78750" -F "country=US" \
  -F "note=Ship flat, this is a jig"

That is the whole integration. It is multipart/form-data rather than JSON because a mesh has to travel with the order, and everything else rides along as form fields. Only the file and the address are genuinely required; the print configuration all has defaults that produce a sensible part.

The Fields That Matter

FieldRequiredWhat it does
fileYesSTL, GLB, .gcode or .gcode.3mf, up to 100 MB
recipientName, street1, city, state, zipYesWhere the part ships. Add street2 for an apartment or suite
countryNoDefaults to US. Also CA, GB, AU, DE, FR, IN, NL, IE, NZ, SE, IT, ES, MX
emailNoOrder updates. Defaults to the account email on the key
materialNopla (default), petg, abs, asa, tpu
qualityNostandard (0.2 mm, 1×) or premium (0.1 mm, 1.75× the rate)
quantityNo1–50. Bulk discounts apply from 5 units
infillNo10–100%, default 20. Meshes only — a sliced file keeps its own
nozzleDiameter, layerHeightNo0.4 mm and 0.20 mm by default. Meshes only
noteNoUp to 1,000 characters, and it travels with the job to the operator

The mesh-only fields are worth flagging. If you send an already-sliced .gcode.3mf, the slicer has already fixed the infill, nozzle and layer height, so those fields are ignored rather than silently re-applied. Sending them anyway is harmless; expecting them to change a sliced file is not.

How the Price Is Decided Before Your Card Is Touched

Two different things happen depending on what you upload, and the difference is large enough to design around.

You sendHow weight is decidedHow time is decidedAccuracy
A sliced .gcode / .gcode.3mfThe grams the slicer measured, purge includedThe slicer's own estimateExact — it is the printer's own plan
An STL or GLB meshMesh volume × infill × material densityVolume ÷ throughput for your nozzle and layer heightClose for solid parts, high for hollow ones

So if your app already slices — or your users hand you a plate out of Bambu Studio — send the sliced file and you are billed the printer's own numbers, multi-colour purge and tool-change time included. /blog/how-to-export-sliced-file-bambu-studio covers producing one. If you only have a mesh, the estimate is deliberately built to sit slightly high rather than slightly low, because a quote that undershoots turns into a conversation nobody wanted.

The rate card itself is short. PLA is $0.12 per gram all-in, filament and normal machine time together, billable weight rounds up to the whole gram, and there is a $1.00 minimum per part. PETG and ABS are also $0.12, TPU $0.13 and ASA $0.14. Premium 0.1 mm layers bill at 1.75×. Shipping is $7 flat in the US and free over $50; international is $30 and free over $500. The full table lives on /pricing.

Here is one real bracket — 90 × 60 × 30 mm, 48 cm³ of solid volume — through four different requests. Everything except the changed field is identical:

RequestBillable gramsPrint hoursPer unitCharged
material=pla, quality=standard27 g1.32$3.24$10.24
material=petg27 g1.32$3.24$10.24
quality=premium, layerHeight=0.1027 g2.64$5.67$12.67
quantity=1027 g each1.32 each$2.92$36.16
Bar chart of per-unit price for the same 27 gram bracket: PLA standard $3.24, PETG standard $3.24, PLA premium 0.1 mm $5.67, and PLA standard at quantity ten $2.92 per unit
Material moves the price a little. Layer height moves it a lot.

Two things fall out of that table. Switching material barely moves anything at this size — a bracket in PETG costs exactly the same as in PLA — so material should be chosen for the job rather than the invoice. Halving the layer height, on the other hand, nearly doubles the price and the machine hours together, which is why premium is worth spending on a visible surface and wasted on a jig.

💡Need a number in your UI before the user has a file? POST /api/price with just a bounding box, material, quality and quantity. It is a coarse upper bound — it assumes the part fills its box — so the same bracket quotes 90 g and $17.80 there against $10.24 for the real mesh. Use it for a ballpark, never for a promise.
curl -X POST https://x3dstudios.com/api/price \
  -H "Content-Type: application/json" \
  -d '{"bboxMm":{"x":90,"y":60,"z":30},"material":"pla","quality":"standard","quantity":1}'

# { "grams": 90, "printHours": 4.46, "materialCost": 10.8, "machineCost": 0,
#   "perUnit": 10.8, "subtotal": 10.8, "shipping": 7, "total": 17.8, "currency": "USD" }

What Comes Back, and What to Store

{
  "code": "X3D-ABCD2345",
  "status": "RECEIVED",
  "paid": true,
  "charged": 10.24,
  "currency": "USD",
  "breakdown": { "print": 3.24, "shipping": 7, "grams": 27, "printHours": 1.32 },
  "statusUrl": "https://x3dstudios.com/print/order/X3D-ABCD2345",
  "apiStatusUrl": "https://x3dstudios.com/api/print/orders/X3D-ABCD2345"
}

Store the code. It is the only handle you need afterwards, it is what your customer support will quote back at you, and statusUrl is a human-readable page you can hand to an end user without giving them an API key. The breakdown is worth persisting too: grams and printHours are the numbers that explain a price six weeks later when somebody asks why two similar orders cost different amounts.

⚠️A 201 does not always mean paid. If the saved card is declined, the order is still created with status PENDING_PAYMENT, paid: false, and a paymentUrl to finish it by hand. Branch on paid, not on the HTTP status code.

Tracking It

Poll GET /api/print/orders/:code with the same key. It returns the current status, the estimated cost, and a tracking object with a number and carrier once the part is on its way. Statuses are a straight line and never go backwards:

StatusWhat it means
PENDING_PAYMENTCreated but not paid — the card failed, use paymentUrl
RECEIVEDPaid, in the queue for review
ACCEPTEDApproved and queued to a printer
PRINTINGOn a machine now
PRINTEDOff the plate and being packed
SHIPPEDHanded to the carrier, tracking available
DELIVEREDCarrier confirmed the drop

Polling every few minutes is pointless. Most orders print, pack and ship inside 24 to 48 hours, so an hourly poll is plenty, and a daily one is fine if all you need is the tracking number. If you want to reason about the timing more carefully, /blog/how-much-does-custom-3d-printing-cost covers what drives both the price and the wait.

Errors Worth Handling

StatusWhat it meansWhat to do
401Invalid or missing API keyCheck the Bearer header; the key may have been rotated
400Missing required shipping fieldsThe response names them — validate before sending
400Country not servedFall back to the supported country list in your UI
413File larger than 100 MBDecimate the mesh or send a sliced file instead
415Unsupported fileSTL, GLB, .gcode or .gcode.3mf only — OBJ is rejected here
422Could not analyse the fileThe mesh is unreadable or has no volume; repair and retry
402CARD_REQUIRED on key generationAdd a card on file, then generate the key

The 415 and 422 cases are the ones a real integration hits, because they come from user-supplied files rather than from your code. Both are worth surfacing verbatim to whoever uploaded the model — the messages name the actual problem, and a user who sent an OBJ can fix it in thirty seconds if you tell them what to export instead.

Skip the Upload Entirely

If the model came out of our own generator, you do not need to download and re-upload several megabytes to order it. POST /api/generate returns a modelUrl for the finished model along with a print-readiness verdict, and the order endpoint accepts that modelUrl in place of a file. Ownership is re-checked server-side rather than taken on trust, so the model has to belong to the account holding the key. The full endpoint reference, including the generation options, is on /api-docs.

That closes a loop that used to need a person in it: your app describes a part, gets a validated mesh back, orders it, and receives a tracking number — all inside one process, with the only physical step happening on a Bambu Lab H2S or P2S in Austin.


FAQ

Do I need a webhook, or is polling fine?

Polling is fine. The status endpoint is cheap, the timeline is measured in hours rather than seconds, and an hourly check catches every transition that matters. The one event worth watching for is SHIPPED, because that is when a tracking number appears.

How small an order can I place?

One part. Billable weight rounds up to the whole gram and there is a $1.00 minimum per part, so a 3 g keyring is $1.00 plus $7 flat US shipping. There is no setup fee, no per-order minimum beyond that, and no discount cliff you have to reach before the API is worth using.

Can I send an OBJ file?

No — the endpoint returns 415 for it. Send an STL or GLB mesh, or an already-sliced .gcode or .gcode.3mf plate. Any modelling tool exports STL in seconds; if you want colour, scale and the printer's own measured weight to survive the trip, slice it in Bambu Studio and send the .gcode.3mf instead.

What happens if the part is too big?

The fleet plate is 340 × 320 × 340 mm, measured in the part's best orientation rather than axis by axis, so a long thin model that fits rotated is not rejected on one dimension. Anything genuinely larger has to be split into pieces and ordered as a multi-part job.

Ready to get started?

Upload a 3D model for instant pricing, or generate one with AI.