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.
The documented endpoints#
| Endpoint | Auth | Limit | Counted per |
|---|---|---|---|
| POST /api/print/orders | API key | None | — |
| GET /api/print/orders/{code} | API key | None | — |
| GET / POST / DELETE /api/account/api-key | Session | None | — |
| POST /api/price | None | None | — |
| POST /api/generate | Session or extension token | 20 per hour | Account email |
| POST /api/generate/start | Session or extension token | 20 per hour | Account email |
| GET /api/generate/status | Session or extension token | None | — |
| POST /api/printability | Session, or none | 60 per hour signed in, 12 per hour anonymous | Account email, or client IP |
| POST /api/slice-quote | Session | 30 per hour | Account email |
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#
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.
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.
| Scope | Limit | Window | Counted per | Where |
|---|---|---|---|---|
| otp-send | 3 | 15 minutes | Email address | Sign-up and password-reset codes |
| generate | 20 | 1 hour | Account email | POST /api/generate |
| generate-start | 20 | 1 hour | Account email | POST /api/generate/start |
| retexture | 30 | 1 hour | Account email | POST /api/retexture |
| image-generate | 40 | 1 hour | Account email | POST /api/images/generate |
| image-edit | 40 | 1 hour | Account email | POST /api/images/edit |
| detect-colors | 30 | 1 hour | Account email | POST /api/detect-colors |
| printability | 60 signed in / 12 anonymous | 1 hour | Account email, or client IP | POST /api/printability |
| print-upload | 40 | 1 hour | Account email | The three /api/print-upload routes, sharing one counter |
| model-import | 20 | 1 hour | Account email | POST /api/model-import |
| slice-quote | 30 | 1 hour | Account email | POST /api/slice-quote |
| cart-reprice | 60 | 1 hour | Account email | POST /api/cart-reprice |
| assistant-chat | 60 signed in / 20 anonymous | 1 hour | Account email, or client IP | The support assistant |
| gift-card-check | 20 | 1 hour | Account email | POST /api/gift-cards/check |
| gift-card-purchase | 20 | 1 hour | Account email | POST /api/gift-cards/purchase |
| agent-order | 12 | 1 hour | Account email | POST /api/agent/order |
| agent-support | 12 | 1 hour | Account email | POST /api/agent/support |
| Scope | Limit | Window | Counted per |
|---|---|---|---|
| farm-print-upload | 24 | 1 hour | Operator email |
| farm-job-create | 36 | 1 hour | Operator email |
| farm-job-reslice | 20 | 1 hour | Operator email |
| farm-order-queue | 30 | 1 hour | Operator email |
| order-message | 60 | 1 hour | Operator email |
| support-attachment | 30 | 1 hour | Operator email |
| operator-email | 1 | 10 minutes | Notification key |
| operator-email-all | 40 | 1 hour | The whole farm |
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.