AI agents
Wiring X3D into an autonomous agent: the three endpoints a tool-calling loop needs, a worked tool schema, and the spend and retry limits you have to enforce yourself.
The print API turns one HTTP call into a physical object at an address. That makes it a good fit for an agent: there is no quoting round-trip, no cart, no checkout page and no human step between the call and the part being made. It also means the call is terminal — it charges a card and starts work — so most of the effort in an agent integration goes into deciding when to make it, not how.
The three endpoints#
/api/priceNo authEstimate a price from a bounding box. No key, no order, nothing charged.
/api/print/ordersAPI keySubmit a file and an address. Charges the card on file and queues the part.
/api/print/orders/{code}API keyRead one order's status, tracking number and cost.
That is the whole surface. There is no cancel endpoint, no list endpoint, no webhook for order state, and no way for a key holder to change an order after it is placed. An agent that needs any of those has to route a human to /contact.
What the agent needs before it can act#
- An X3D account with a card on file. The card is required to mint a key, and it is the card every order is charged to.
- A key from /profile. It looks like x3d_live_ followed by 48 hex characters. It is shown once and stored only as a hash, so put it in your secret manager at the moment it is issued.
- A complete shipping address. The API validates that the required fields are present and that the country is one we ship to. It does not validate that the address exists.
Authorization: Bearer x3d_live_8f2b41c07d9e5a3610bc7f4d82e91a05c36bd7f2ae04915cAn x-api-key header carrying the same value works too — it is read only when the Authorization header does not match the Bearer pattern. See /docs/api/authentication for rotation and revocation.
The minimum viable tool definition#
One tool is enough to place an order, but a three-tool set is what makes an agent behave sensibly: price first, order once, then poll. Keep the price tool separate from the order tool — an agent that can only order will order to find out what something costs.
[
{
"name": "x3d_estimate_price",
"description": "Estimate the cost of 3D printing a part from its bounding box in millimetres. Charges nothing and creates nothing. Use this before proposing an order.",
"input_schema": {
"type": "object",
"properties": {
"x_mm": { "type": "number", "description": "Width in mm" },
"y_mm": { "type": "number", "description": "Depth in mm" },
"z_mm": { "type": "number", "description": "Height in mm" },
"material": { "type": "string", "enum": ["pla", "petg", "abs", "asa", "tpu"] },
"quality": { "type": "string", "enum": ["standard", "premium"] },
"quantity": { "type": "integer", "minimum": 1, "maximum": 50 },
"color": { "type": "string", "description": "A stock colour name, e.g. Black" }
},
"required": ["x_mm", "y_mm", "z_mm"]
}
},
{
"name": "x3d_place_print_order",
"description": "Place a real 3D print order. This charges the card on file immediately and cannot be cancelled through the API. Only call after a human has confirmed the price and the shipping address.",
"input_schema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "Local path to an .stl, .glb, .gcode or .gcode.3mf file" },
"material": { "type": "string", "enum": ["pla", "petg", "abs", "asa", "tpu"] },
"quality": { "type": "string", "enum": ["standard", "premium"] },
"quantity": { "type": "integer", "minimum": 1, "maximum": 50 },
"color": { "type": "string" },
"recipientName": { "type": "string" },
"street1": { "type": "string" },
"street2": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"zip": { "type": "string" },
"country": { "type": "string", "default": "US" }
},
"required": ["file_path", "recipientName", "street1", "city", "state", "zip"]
}
},
{
"name": "x3d_get_order_status",
"description": "Read the current status and tracking number of an X3D order by its code, e.g. X3D-K7M2QP.",
"input_schema": {
"type": "object",
"properties": { "code": { "type": "string" } },
"required": ["code"]
}
}
]The call sequence#
- 1Price the part
POST /api/price with the bounding box. It is public, so no key leaves your secret store on this call. Read subtotal, not total — see the warning below.
bashcurl -X POST https://x3dstudios.com/api/price \ -H "Content-Type: application/json" \ -d '{"bboxMm":{"x":60,"y":40,"z":25},"material":"pla","quality":"standard","quantity":1,"color":"Black"}' - 2Get a human to confirm
Show the estimate and the full shipping address, and wait for a yes. This is the only confirmation step that exists — nothing downstream asks again.
- 3Submit the order
POST /api/print/orders as multipart/form-data. The endpoint does not accept a JSON body. On success you get 201 with a code, the amount charged, and statusUrl.
bashcurl -X POST https://x3dstudios.com/api/print/orders \ -H "Authorization: Bearer $X3D_API_KEY" \ -F "[email protected]" \ -F "material=pla" \ -F "color=Black" \ -F "quality=standard" \ -F "quantity=1" \ -F "recipientName=Ada Lovelace" \ -F "street1=1 Main St" \ -F "city=Austin" \ -F "state=TX" \ -F "zip=78750" \ -F "country=US" - 4Store the code immediately
Write the code to your own store before you do anything else. There is no list endpoint, so a code your agent forgets is a code your agent cannot look up again. Orders placed through the API also do not appear on the /orders page — each one has its own page at statusUrl instead.
- 5Poll for status
GET /api/print/orders/{code} with the same key. Poll on a schedule measured in hours, not seconds — a print takes hours and the status field only moves when a person or a printer moves it.
bashcurl https://x3dstudios.com/api/print/orders/X3D-K7M2QP \ -H "Authorization: Bearer $X3D_API_KEY"
Fields worth setting explicitly#
colorstringoptionaldefaultDefault- Omitting it is expensive. The default value is the literal string Default, which is not a stock colour, so the order is priced at the $0.20/g custom-colour floor instead of the material rate. Always send a stock name — Black, White, Red, Cyan, Sakura Pink, Sunny Orange, Green, Yellow, Mint Green, Lavender, Silver or Brown.
quantityintegeroptionaldefault1- Clamped to 1-50, never rejected. A model that emits 500 gets an order for 50, with no error to notice. Validate the number in your own tool before sending it.
materialstringoptionaldefaultpla- Rejected with 400 if it is not one of these five. This is one of the few fields that fails loudly.
plapetgabsasatpu nozzleDiameternumberoptionaldefault0.4- Anything else silently falls back to 0.4. No error is returned, so a wrong value looks like a successful order.
0.20.40.60.8 notestringoptional- Truncated to 1000 characters without warning. Do not use it to carry structured data your agent expects back — nothing reads it back to you.
countrystringoptionaldefaultUS- US plus CA, GB, AU, DE, FR, IN, NL, IE, NZ, SE, IT, ES and MX. Anything else is 400 with the message naming the country.
Safety guidance for agent builders#
There is no idempotency key#
The awkward case is a request that fails after the order was created — a dropped connection, a proxy timeout. You have no code, and there is no list endpoint to go and look for the order you might have placed. There is no safe programmatic recovery from that state. Log it and stop. A successful charge sends a confirmation email carrying the order code, so the code is recoverable by a human reading the account's inbox; if it does not arrive, ask us at /contact rather than resubmitting.
Note that a failed card charge is not a failed request. If the card is declined the response is still 201 with an order attached, paid set to false, status PENDING_PAYMENT and a paymentUrl a human can open. Resubmitting that order duplicates it. Check the paid field rather than only the HTTP status.
Spend limits are yours to build#
| Control | Enforced by X3D? | What actually exists |
|---|---|---|
| Per-key spend cap | No | None. A valid key can charge the card as many times as it is asked to. |
| Rate limit on the print API | No | Neither POST /api/print/orders nor the status endpoint is rate limited in the application. |
| Per-order ceiling | No | Quantity is capped at 50 per order, but nothing caps the value of an order or the number of orders. |
| Order minimum | Yes | An order under $0.50 is rejected with 400. |
| Card requirement | Yes | A key cannot be minted without a card on file, and the card is charged at submission. |
- Keep a running total in your own store and refuse to call the tool past a budget you set.
- Require a fresh human confirmation for any order above a threshold, and for the first order to any new address.
- Give the agent a key you can rotate quickly. Generating a new key at /profile invalidates the old one immediately, which is the fastest kill switch available.
Confirm the address, because we do not#
The API checks that recipientName, street1, city, state and zip are present and that the country is on the list. It does not check that the address is real, that the postcode matches the state, or that the phone number is a phone number. A hallucinated street number is accepted, printed and posted.
- Echo the full address back to the human in the confirmation step, formatted as it will be printed — not as a summary.
- Prefer an address the human supplied in this session over one the agent recalled from context.
- The status endpoint returns shipTo with only name, city, state and country. Street and postcode are deliberately not readable back, so your own record is the only copy the agent can verify against.
Nothing is reversible through the API#
There is no cancel call and no refund call. Once an order is submitted and paid it is queued at the farm, and changing or stopping it is a conversation with a person. Build that into the agent's prompt: the order tool is the point of no return, and the agent should say so before calling it.
Key format, the two headers, rotation and what happens when a key is lost.
Every accepted field on POST /api/print/orders and the full response shape.
The exact status codes and messages the print API returns, and what to do about each.
The nine status values, what moves them, and how tracking appears.