Skip to content

Rate limits and error handling

An HTTP response and an asynchronous job result are different layers. Inspect both.

{
"error": "try_on_limit_reached",
"code": "TRY_ON_LIMIT_REACHED",
"message": "Come back tomorrow for more!"
}

error is always present. code and message are present only for some failures. Branch on the status and code when code exists, and on the error string when it does not.

SituationResponse
Product is not in the funnel403 { "error": "Product is not assigned to this funnel" }
Email required403 { "error": "email_required", "code": "EMAIL_REQUIRED" }
Customer try-on limit429 { "error": "try_on_limit_reached", "code": "TRY_ON_LIMIT_REACHED", "message": "Come back tomorrow for more!" }
Shared IP limit429 { "error": "Rate limit exceeded", "message": "Too many requests..." } and a Retry-After header
Credits or queue failure on POST /generations500 { "error": "Could not create generation" }

The try-on limit message is the merchant’s text. The sample above is the default. Show message to the shopper when it is present. Render it as text.

An inaccessible photo can fail after POST /generations has already returned a job ID. Generation does not reject a sold-out variant.

StatusMeaningClient action
200Successful HTTP operation; a polled job can still be ERRORInspect data.
204OPTIONS preflight responseNo JSON body.
400Invalid input/JSON, missing product image or visitor registration problemCorrect the request; do not repeat unchanged.
401Missing, invalid, refreshed or disconnected SDK keyFix connection/environment configuration.
403Product not in the Funnel, EMAIL_REQUIRED, or SDK_NOT_CONFIGURED on /contextRead code. Funnel mismatch has no code. API-only shops skip /context.
404Product, variant, funnel, or job is missing or in another storeCheck IDs against this SDK key’s store.
405Unsupported HTTP methodUse the method documented for that path.
429Shared IP limit, or TRY_ON_LIMIT_REACHEDHonor Retry-After only when the body is the IP limit.
500Shopify, credits, queue, upload target, or a stored result that cannot be readKeep a known job ID. Do not automatically resubmit a generation POST.

Every Headless route shares one counter, keyed by client IP, with the other Headless routes and with Antla’s theme app proxy. A poll increments the same count as a generation. The number in the table is the highest shared count that route will still accept. It is not a private budget.

OperationRejected when the shared IP count in 60 seconds exceeds
Start generation120
Poll generation240
Create upload target120
Resolve funnel120
Get email status120
Register customer60
Get Storefront context120

A shopper network, or a backend that sends every shopper through one egress IP, shares that counter too. Pass each shopper’s uniqueUserId so visitor identity does not fall back to that shared IP.

An IP rate-limit response includes Retry-After in seconds. Browser JavaScript can read that header. A customer try-on limit is also HTTP 429 and does not include Retry-After.

Operation/outcomeRecommended behavior
GET poll: transient transport error or 5xxRetry with a bounded backoff while preserving the same job ID and UI deadline.
GET poll: IP 429 with Retry-AfterWait the specified interval within your deadline; do not create a new job.
POST generation: explicit EMAIL_REQUIREDCollect email, then retry that rejected submission with the required input.
POST generation: explicit IP throttlingWait, then retry the same attempt. Do not start a second job.
POST generation: network timeout, lost response or 5xxOutcome may be unknown. Do not auto-resubmit.
Job ERRORStop polling; show failure and let the user choose a new attempt.
TRY_ON_LIMIT_REACHEDShow the merchant’s message. Do not run a retry loop.
Invalid/malformed successful responseStop and report a protocol problem; do not infer a job ID or final URL.

A POST can start work even if the client never receives a response. The API has no documented idempotency-key mechanism, cancellation endpoint or lookup by a client-generated request ID.

Use an overall polling deadline and a per-request timeout. Cancel browser/native requests when the relevant screen is abandoned. This stops client work; it does not cancel Antla’s queued job.

If you already have a job ID, allow resuming its status check. A UI timeout is not proof that generation failed. Do not discard a temporary preview’s pending status just because your UI deadline elapsed.

SymptomCheck
SDK does not appearScript URL, key, product ID, Storefront product visibility, product image and funnel tag.
401 on every requestMatching environment, active key and Headless SDK or API enabled.
403 SDK_NOT_CONFIGURED on /contextExpected for API-only. Use your catalog, or connect Headless SDK.
Funnel 404Candidate tags are current and belong to this store.
Generation 403Exact product/funnel assignment; handle EMAIL_REQUIRED.
Generation 404Product/variant pair, funnel ownership and correct store key.
Upload PUT failsTarget expiry, matching MIME type, raw bytes, no SDK header, storage CORS where applicable.
API works but cart page is emptyStorefront and SDK share the same complete cart ID/session.
Theme event listener never firesSDK does not implement the theme event contract.
Many 429s behind a backendShared IP counter, polling concurrency and visitor identity design.
Photo remains temporaryContinue polling; handle storage failure and timeout separately.

For support, collect environment, endpoint, method, HTTP status, error code, approximate time and a job ID through an appropriate private channel. Redact keys, tokens, email, photo URLs and complete cart IDs.