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

Source: https://x3dstudios.com/docs/api/rate-limits  ·  Updated: 2026-10-16

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, and the MCP server — 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

| 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, or a /print visitor cookie | 30 per hour at 100%, and 30 new sizes an hour. Signed out: 20 an hour per browser, 40 an hour and 120 a day per connection | Account email, or the visitor and their connection |
| POST /api/mcp | API key, or none | 600 requests an hour, then a budget per kind of tool: see /docs/api/mcp | Account, or the connecting address |

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

| 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 |
| print-resume | 30 | 1 hour | Account email | POST /api/print-upload/resume |
| model-import | 20 | 1 hour | Account email | POST /api/model-import |
| slice-quote | 30 | 1 hour | Account email | POST /api/slice-quote at 100% |
| scaled-slice | 30 | 1 hour | Account email, or visitor | Every new size or TPU slice weighed, on any page that weighs one, ordering included |
| multicolor-slice | 20 | 1 hour | Account email, or visitor | POST /api/multicolor-slice, the colour check on /print |
| edit-mesh | 120 | 1 hour | Account email | POST /api/edit-mesh |
| cart-reprice | 60 | 1 hour | Account email | POST /api/cart-reprice |
| promo-check | 20 | 1 hour | Account email | POST /api/promo/check |
| pay-link | 30 | 1 hour | Payment link | POST /api/pay/{id}, the Pay button on a payment link |
| review-submit | 10 | 1 hour | Order | POST /api/reviews |
| fleet-request | 2 | 1 day | Account email | Asking for printers on the farm dashboard |
| 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 |
| mcp:endpoint | 600 | 1 hour | Account, or connecting address | Every request to POST /api/mcp, counted first |
| mcp:read | 300 | 1 hour | Account, or connecting address | MCP tools that only read |
| mcp:fetch | 40 | 1 hour | Account, or connecting address | check_printability and quote_print |
| mcp:credits | 20 | 1 hour | Account, or connecting address | generate_model, which also spends the generate-start limit above |
| mcp:money | 10 | 1 hour | Account, or connecting address | create_print_order |
| mcp:track-miss | 10 | 1 hour | Email asked about | track_order lookups that found nothing |
| mcp:malformed | 30 | 1 hour | Connecting address | Bodies the MCP endpoint could not read |

*Customer-reachable limits. quote_print also keeps its own cap of 30 quotes an hour per account.*

A signed-out visitor on /print has limits of its own, kept apart from the table above: per browser, per connection, and a daily ceiling shared by everyone signed out, on uploading, importing a model from a link, the colour check and pricing. Each refusal is a 429 with Retry-After and signIn: true, and its message says which limit was reached. Signing in replaces them with the account limits.

| Scope | Limit | Window | Counted per |
| --- | --- | --- | --- |
| farm-print-upload | 24 | 1 hour | Operator email |
| farm-model-check | 30 | 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 |

*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 with no REST endpoint that lists orders, concurrent submissions are the easiest way to end up with duplicate orders you then have to find by hand on /orders.

- [Errors](https://x3dstudios.com/docs/api/errors) — Every status code and message, and which failures are safe to retry.
- [Webhooks and polling](https://x3dstudios.com/docs/api/webhooks) — Why nothing is pushed to you, and how to write the polling loop instead.
- [Farm capacity](https://x3dstudios.com/docs/farm/capacity) — What the fleet can actually get through in a day.

## Frequently asked

**Is there a rate limit on the X3D print API?**

Not in the application. POST /api/print/orders, GET /api/print/orders/{code} and the key-management endpoints call no limiter, so they never return 429. That is a description of today's code, not a service guarantee — the hosting layer in front of them is outside it.

**How long should I wait after a 429 from X3D?**

Read the Retry-After header. It is a whole number of seconds until the current window resets, at least 1. Most endpoints send it. A few return 429 without it: /api/slice-quote at 100% for a signed-in account, /api/detect-colors, /api/cart-reprice, the payment-link and review routes, and the fleet-access request. Fall back to a minute there.

**Do failed requests count against my hourly limit?**

A request refused with 429 does not increment the counter — being over the limit does not extend the lockout. A request that passes the limiter and then fails for another reason, such as a 400, has already spent its slot.
