RorlAI API
Generate on-model images from your PIM, your ERP or your own build script. Same credits, same account, same models as the studios.
Quickstart
Five minutes from signup to your first image. Start with a test key: it returns fixture images, completes in about three seconds, and charges nothing.
1. Create a key
Go to Settings, API keys and create one. Pick Test mode while you build. The key is shown once.
2. Submit a generation
curl -X POST https://app.rorlai.app/api/v1/tryon/apparel \
-H "Authorization: Bearer rrl_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"product_type": "dress",
"product_images": ["https://cdn.example.com/dress.jpg"],
"resolution": "2K",
"aspect_ratio": "3:4"
}'
# 202 Accepted
# { "data": { "id": "…", "status": "queued", "credits_charged": 0,
# "estimated_seconds": 3, "sandbox": true }, "request_id": "req_…" }3. Poll until it finishes
Every capability polls the same endpoint, so you write this loop once.
curl https://app.rorlai.app/api/v1/generations/GENERATION_ID \
-H "Authorization: Bearer rrl_test_YOUR_KEY"
# 200 OK
# { "data": { "id": "…", "status": "completed",
# "outputs": [{ "url": "https://…", "width": 2048, "height": 2731 }],
# "credits_charged": 0, "duration_ms": 3000 }, "request_id": "req_…" }4. Or take a webhook instead
Pass webhook_url on the request, or set a default on the key. We sign every delivery with HMAC-SHA256 over {timestamp}.{body} and retry five times: after 1m, 5m, 30m, 2h and 6h.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const age = Math.floor(Date.now() / 1000) - Number(parts.t);
if (!Number.isFinite(age) || Math.abs(age) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}How it works
- Async everywhere. Every POST returns a job id in well under a second. Nothing holds a connection open for four minutes.
- One pattern for every endpoint. Same auth, same submit-and-poll shape, same error envelope. Learn one, ship all of them.
- Shared credits. One balance across the app and the API, and one ledger to reconcile.
- A real sandbox. Test keys exercise your whole integration, including the poll loop and webhooks, before you spend a credit.
Endpoints
| POST /api/v1/tryon/apparel | Put a garment on a model |
| POST /api/v1/tryon/jewelry | Place jewellery on a body-part model |
| POST /api/v1/tryon/eyewear | Place eyewear on a face model |
| POST /api/v1/tryon/shoe | Place footwear on a foot model |
| POST /api/v1/ghost-mannequin | Invisible-mannequin render from a flat or worn garment photo |
| POST /api/v1/product/scene | Place a product in a generated scene |
| POST /api/v1/background/remove | Cut the product out of its background |
| POST /api/v1/upscale | Increase resolution without inventing detail |
| POST /api/v1/pose-change | Re-pose an existing on-model image |
| GET /api/v1/generations/:id | Poll any generation |
| DELETE /api/v1/generations/:id | Cancel and refund |
Resources
| GET /api/v1/mannequins | Models, filterable by kind, gender and size |
| GET /api/v1/poses | Poses, filterable by product type |
| GET /api/v1/backgrounds | Backdrops |
| GET /api/v1/credits | Your balance, shared with the app |
| GET /api/v1/usage | Requests and credits by endpoint |
Rate limits
Requests a minute and concurrent generations, by plan. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After.
| Plan | Requests / min | Concurrent |
|---|---|---|
| free | — | — |
| hobby | — | — |
| seller | 30 | 2 |
| brand | 30 | 5 |
| studio | 60 | 15 |
Errors
One envelope everywhere. Write the handler once. Every response carries a request_id, in the body and in the X-Request-Id header — quote it if you contact us.
{
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "This request requires 5 credits. Your balance is 2.",
"type": "billing_error",
"docs_url": "https://docs.rorlai.app/errors#insufficient_credits",
"request_id": "req_…"
},
"request_id": "req_…"
}| UNAUTHORIZED | 401 | authentication_error |
| KEY_REVOKED | 401 | authentication_error |
| FORBIDDEN | 403 | authorization_error |
| PLAN_NO_API | 403 | authorization_error |
| NOT_FOUND | 404 | not_found_error |
| INVALID_INPUT | 422 | invalid_request_error |
| INSUFFICIENT_CREDITS | 402 | billing_error |
| PLAN_LIMIT | 403 | authorization_error |
| RATE_LIMITED | 429 | rate_limit_error |
| CONCURRENCY_LIMIT | 429 | rate_limit_error |
| MODERATION_BLOCKED | 422 | moderation_error |
| PROVIDER_ERROR | 502 | provider_error |
| PROVIDER_TIMEOUT | 504 | provider_error |
| QUALITY_REJECTED | 422 | invalid_request_error |
| CAPABILITY_UNAVAILABLE | 503 | provider_error |
| URL_EXPIRED | 403 | authorization_error |
| INTERNAL | 500 | server_error |
Output URLs
Output URLs are signed and valid for seven days, then return 403. Copy the bytes to your own storage as soon as you receive them. Add ?download=1 to any output URL to get it as a file attachment.
OpenAPI 3.1
The full machine-readable spec. Point your generator at it for a typed client in any language.
Download the spec