Two limits apply to your keys: a pace per key, and monthly caps per plan. Both answer 429 with a JSON envelope. The network edge in front of the API adds a third, coarser one that answers without JSON.
Pace
The default plan allows 2 requests a second per key with bursts up to 10; your plan may allow more, and GET /v1/ping reports your rate_limit. Over the limit you get 429 RATE_LIMITED with Retry-After in seconds, X-RateLimit-Limit and X-RateLimit-Remaining. Throttled calls are logged like any other and counted in your usage.
Monthly caps
Your plan may cap requests, live orders and test orders per calendar month (UTC). GET /v1/account shows the caps under limits — monthly_request_cap, monthly_order_cap, monthly_test_order_cap, where 0 means no cap — and this month's usage. At 80 percent of a cap you get usage.warning once; at the cap, 429 QUOTA_EXCEEDED until the first of next month. The request cap is checked against a count refreshed every 30 seconds, so a handful of calls may pass over it. A quote cap per day (quote_daily_cap) works the same way.
The edge throttle
In front of the API sits a network edge that protects every LayerCrew service. Above roughly 5 requests a second from one address it answers 403 with an HTML or empty body — no JSON envelope, no X-Request-ID.
- Treat a
403without JSON as the edge: back off 90 seconds, then resume at two requests a second. - Keep to two requests a second per source address, with small bursts, even when your plan's key limit is higher.
- Never retry a create blindly. For an order, send the same
Idempotency-Keyagain or look it up byreference; for a file, list your files; for a webhook endpoint, list your endpoints.
Designing for the limits
- Use webhooks for state changes and the events feed to reconcile; polling each order is the pattern that hits the limits first.
- Batch your reads: lists take
per_pageup to 100, the feedlimitup to 200. - One client, one queue: a shared token bucket in your integration keeps bursts in bounds.
- Back off on
429and on5xxwith jitter; quoteX-Request-IDif a pattern repeats.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function layercrew(path, init = {}, attempt = 0) {
const res = await fetch(`https://engine.layercrew.com/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.LAYERCREW_API_KEY}`, ...(init.headers ?? {}) },
});
const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
if (res.status === 403 && !isJson) {
await sleep(90_000); // the edge: back off 90 seconds
return layercrew(path, init, attempt + 1); // same init, same Idempotency-Key
}
if ((res.status === 429 || res.status >= 500) && attempt < 5) {
const retryAfter = Number(res.headers.get("retry-after")) || 2 ** attempt;
await sleep(retryAfter * 1000 + Math.random() * 500);
return layercrew(path, init, attempt + 1);
}
return res;
}A fetch wrapper that honours Retry-After, backs off the edge and never retries a create without its key
Body size
Request bodies are limited to 256 KiB — 413 PAYLOAD_TOO_LARGE beyond it. File bytes never travel in JSON; they go to the signed upload URL described under Files.