X3DStudios

Rate limits

What is rate limited at X3D and what is not: the print API endpoints carry no application limit, while generation and printability cap you per hour.

The endpoints that submit and read print orders have no rate limit in the application. Nothing in POST /api/print/orders, GET /api/print/orders/{code} or /api/account/api-key calls the limiter, so none of them can return 429. Other endpoints documented in this section — generation, printability, quoting from a stored model — do count your requests, per hour, per account.

No limit in code is not a promise of no limit
This page describes what the application does. The site runs behind hosting infrastructure that can refuse traffic on its own terms, and an unlimited endpoint that charges a card on every call is not an invitation to loop over it. Treat the absence of a limiter as the absence of a safety net, not as capacity.

The documented endpoints#

EndpointAuthLimitCounted per
POST /api/print/ordersAPI keyNone
GET /api/print/orders/{code}API keyNone
GET / POST / DELETE /api/account/api-keySessionNone
POST /api/priceNoneNone
POST /api/generateSession or extension token20 per hourAccount email
POST /api/generate/startSession or extension token20 per hourAccount email
GET /api/generate/statusSession or extension tokenNone
POST /api/printabilitySession, or none60 per hour signed in, 12 per hour anonymousAccount email, or client IP
POST /api/slice-quoteSession30 per hourAccount email
Key rotation is not throttled either — POST /api/account/api-key can be called as often as you like, and each call invalidates the previous key.
Limits are per account, not per credential
The subject of every per-user limit is the lowercased account email. A browser session and a Chrome-extension token belonging to the same account share one bucket, so generating from the website spends the same 20 per hour as generating from the extension. Issuing a second credential does not buy you a second allowance.

How the window works#

The limiter is a fixed window, not a rolling one. The window start is the clock floored to the window length, so an hourly limit resets on the hour for everybody at once, and a 15-minute limit resets at :00, :15, :30 and :45. Your first request does not start your own private hour.

Two consequences are worth designing around. A burst at the boundary is allowed: 20 generations at 10:59 and 20 more at 11:00 is 40 in two minutes without ever breaching a 20-per-hour limit. And an allowance spent early in a window is gone for the rest of it, however quiet the rest of that hour is — there is no drip of credit back.

  • The counter key is a hash of the scope, the subject and the window start. Scopes are independent: spending your generation allowance leaves printability untouched.
  • A request refused with 429 does not increment the counter, so hammering a closed door does not push the reset further out.
  • A request that passes the limiter and then fails for its own reasons has still spent a slot. A 400 costs you one.
  • Counters live in the same database the rest of the site uses, and the limiter fails closed: if that database is unreachable in production, requests are denied rather than waved through. A burst of 429s can therefore mean an outage rather than your traffic.

The 429 response#

A limited endpoint refusing a request
HTTP/1.1 429 Too Many Requests
Retry-After: 3287
Content-Type: application/json

{ "error": "Generation limit reached. Please try again later." }

Retry-After is whole seconds until the window resets, never less than 1. It is a countdown to a fixed clock boundary, so it shrinks as the window ages — a 429 at 10:05 against an hourly limit reports about 3300 seconds, and the same 429 at 10:59 reports about 60.

Three endpoints send no Retry-After
POST /api/slice-quote, POST /api/detect-colors and POST /api/cart-reprice return 429 with the error body and no Retry-After header. If you call them, do not assume the header is there — wait a minute and try again.

Every limit in the codebase#

These are all of them, with the numbers as they are written in the routes. The first group is reachable by a customer; the second needs an operator account and is listed for completeness.

ScopeLimitWindowCounted perWhere
otp-send315 minutesEmail addressSign-up and password-reset codes
generate201 hourAccount emailPOST /api/generate
generate-start201 hourAccount emailPOST /api/generate/start
retexture301 hourAccount emailPOST /api/retexture
image-generate401 hourAccount emailPOST /api/images/generate
image-edit401 hourAccount emailPOST /api/images/edit
detect-colors301 hourAccount emailPOST /api/detect-colors
printability60 signed in / 12 anonymous1 hourAccount email, or client IPPOST /api/printability
print-upload401 hourAccount emailThe three /api/print-upload routes, sharing one counter
model-import201 hourAccount emailPOST /api/model-import
slice-quote301 hourAccount emailPOST /api/slice-quote
cart-reprice601 hourAccount emailPOST /api/cart-reprice
assistant-chat60 signed in / 20 anonymous1 hourAccount email, or client IPThe support assistant
gift-card-check201 hourAccount emailPOST /api/gift-cards/check
gift-card-purchase201 hourAccount emailPOST /api/gift-cards/purchase
agent-order121 hourAccount emailPOST /api/agent/order
agent-support121 hourAccount emailPOST /api/agent/support
Customer-reachable limits.
ScopeLimitWindowCounted per
farm-print-upload241 hourOperator email
farm-job-create361 hourOperator email
farm-job-reslice201 hourOperator email
farm-order-queue301 hourOperator email
order-message601 hourOperator email
support-attachment301 hourOperator email
operator-email110 minutesNotification key
operator-email-all401 hourThe whole farm
Operator and internal limits. No API key can reach these routes.

What actually constrains a print integration#

Since the print endpoints do not count your calls, the real ceilings are elsewhere, and they matter more than a request budget would.

  • Money. Every accepted submission charges your card immediately. A runaway loop is a run of real charges and real orders, not a run of 429s.
  • Farm capacity. Orders queue behind other orders on a small fleet in Austin. Submitting a hundred parts in a minute does not print them any sooner — see /docs/farm/capacity.
  • Polling is cheap but not free. Prints run for hours, so a status poll every few minutes tells you everything a poll every second would.
Serialise your submissions
One request at a time, and check the response before sending the next. Without an idempotency key and without a list endpoint, concurrent submissions are the easiest way to end up with duplicate orders you cannot reconcile.