X3DStudios

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#

POST/api/priceNo auth

Estimate a price from a bounding box. No key, no order, nothing charged.

POST/api/print/ordersAPI key

Submit a file and an address. Charges the card on file and queues the part.

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

Read 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#

  1. 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.
  2. 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.
  3. 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.
The header the agent sends on every call
Authorization: Bearer x3d_live_8f2b41c07d9e5a3610bc7f4d82e91a05c36bd7f2ae04915c

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

Tool schemas
[
  {
    "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#

  1. 1
    Price 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.

    bash
    curl -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"}'
  2. 2
    Get 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.

  3. 3
    Submit 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.

    bash
    curl -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"
  4. 4
    Store 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.

  5. 5
    Poll 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.

    bash
    curl https://x3dstudios.com/api/print/orders/X3D-K7M2QP \
      -H "Authorization: Bearer $X3D_API_KEY"
POST /api/price always adds $7 postage to its total
The price endpoint returns a full breakdown, but its total line adds the $7 flat rate unconditionally. The order path waives postage at a $50 print subtotal and above, so from $50 up the price endpoint's total is $7 high. Take subtotal from the estimate and apply postage yourself: $7 in the US, free at $50 and above; $30 international, free at $500 and above.
The estimate is a bounding-box guess, the order price is measured
POST /api/price only knows the box you gave it, so a hollow or thin part estimates high. The order price comes from reading the actual file — the mesh's own volume, or a pre-sliced file's own filament figure — which is why estimatedCost in the 201 can differ from the estimate. Treat the estimate as a budget check, not a quote.

Fields worth setting explicitly#

Submission fields an agent tends to get wrong
colorstringoptionaldefault Default
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.
quantityintegeroptionaldefault 1
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.
materialstringoptionaldefault pla
Rejected with 400 if it is not one of these five. This is one of the few fields that fails loudly.plapetgabsasatpu
nozzleDiameternumberoptionaldefault 0.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.
countrystringoptionaldefault US
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#

A retried submission is a second order and a second charge
POST /api/print/orders has no Idempotency-Key header and no request deduplication. If your HTTP client retries on a timeout, you get two orders, two parts and two charges. Turn automatic retries off on this endpoint specifically.

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#

ControlEnforced by X3D?What actually exists
Per-key spend capNoNone. A valid key can charge the card as many times as it is asked to.
Rate limit on the print APINoNeither POST /api/print/orders nor the status endpoint is rate limited in the application.
Per-order ceilingNoQuantity is capped at 50 per order, but nothing caps the value of an order or the number of orders.
Order minimumYesAn order under $0.50 is rejected with 400.
Card requirementYesA 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.