Python · Virtual try-on API

Virtual try-on API in Python

In one sentence

Install requests, load PHOTTA_API_KEY from env, write a photta() wrapper that sends the Bearer key and raises a typed PhottaError on non-2xx responses, then POST to /api/v1/tryon/apparel and poll /api/v1/tryon/apparel/{id} every 3 seconds until status is completed.

  • Typical time: 1.5–4 min
  • Credits: 5 credits at 2K · 6 credits at 4K
  • Processing: async, polling
  • Limits: 600 requests/min
  • Authentication: Bearer key

Every figure on this page comes from the Photta API docs · checked

POST/api/v1/tryon/apparel
import os, requests

res = requests.post(
    "https://api.photta.app/api/v1/tryon/apparel",
    headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"},
    json={
      "product_type": "top",
      "product_images": [
        "https://cdn.yourstore.com/products/shirt-front.jpg"
      ],
      "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
      "product_category": "t_shirt",
      "mannequin_id": "mnk_f3a9b1",
      "pose_id": "pose_c2d4e5",
      "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
      "lock_background": True,
      "resolution": "2K",
      "aspect_ratio": "3:4",
      "body_size": "M",
      "expression": "neutral",
      "user_instruction": "Keep the collar pattern crisp and centered."
    },
)
data = res.json()["data"]  # data["id"], data["status"]

import time

def wait_for(job_id):
    for _ in range(120):
        res = requests.get(f"https://api.photta.app/api/v1/tryon/apparel/{job_id}", headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"})
        data = res.json()["data"]
        if data["status"] == "completed":
            return data["output_url"]
        if data["status"] == "failed":
            raise RuntimeError(data.get("error_message", "generation failed"))
        time.sleep(3)
    raise TimeoutError("timed out after 6 minutes")

At a glance

At a glance

Endpoint
POST /api/v1/tryon/apparel
Authentication
Bearer API key
Processing
Asynchronous — poll the status endpoint
Typical time
1.5–4 min
Credits
5 credits at 2K · 6 credits at 4K
Aspect ratios
3:4 · 1:1 · 4:3 · 9:16 · 16:9
Input limits
Up to 10 MB per image; JPEG, PNG, WebP
Idempotent
No

How it works

Five steps, zero Photta-specific dependencies — requests covers everything.

  1. Step 1

    Sign up and generate a key

    Same path as every other language: ai.photta.app → Developers → Generate API key. Keys start with `photta_live_` and work across every Photta capability.

  2. Step 2

    Load the key from env

    Set `PHOTTA_API_KEY` in the environment and read it via `os.environ`. In FastAPI or Django, load it once at boot into a settings module. Don't embed the key in source code or public Git history.

    python3 -m pip install requests
    export PHOTTA_API_KEY="photta_live_xxxxx"
  3. Step 3

    Write a requests wrapper

    A thirty-line `photta_request()` function handles the base URL, the Authorization header, JSON serialisation and error translation. That's the only abstraction you need until the Python SDK lands.

    # photta.py — one helper for every endpoint
    import os, requests
    BASE = "https://api.photta.app/api/v1"
    
    class PhottaError(Exception):
        def __init__(self, status, error, retry_after=None):
            super().__init__(error.get("message", "request failed"))
            self.status, self.type, self.retry_after = status, error.get("type"), retry_after
    
    def photta(method, path, **kwargs):
        res = requests.request(method, BASE + path, headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"}, timeout=30, **kwargs)
        body = res.json()
        if not res.ok:
            raise PhottaError(res.status_code, body.get("error", {}), res.headers.get("Retry-After"))
        return body
  4. Step 4

    Submit and poll

    POST to `/api/v1/tryon/apparel` to get a generation ID, then poll `/api/v1/tryon/apparel/{id}` on a 3-second interval. In Celery or RQ, break the poll loop across tasks so a multi-minute job does not tie up a worker.

    import os, requests
    
    res = requests.post(
        "https://api.photta.app/api/v1/tryon/apparel",
        headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"},
        json={
          "product_type": "top",
          "product_images": [
            "https://cdn.yourstore.com/products/shirt-front.jpg"
          ],
          "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
          "product_category": "t_shirt",
          "mannequin_id": "mnk_f3a9b1",
          "pose_id": "pose_c2d4e5",
          "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
          "lock_background": True,
          "resolution": "2K",
          "aspect_ratio": "3:4",
          "body_size": "M",
          "expression": "neutral",
          "user_instruction": "Keep the collar pattern crisp and centered."
        },
    )
    data = res.json()["data"]  # data["id"], data["status"]
    
    import time
    
    def wait_for(job_id):
        for _ in range(120):
            res = requests.get(f"https://api.photta.app/api/v1/tryon/apparel/{job_id}", headers={"Authorization": f"Bearer {os.environ['PHOTTA_API_KEY']}"})
            data = res.json()["data"]
            if data["status"] == "completed":
                return data["output_url"]
            if data["status"] == "failed":
                raise RuntimeError(data.get("error_message", "generation failed"))
            time.sleep(3)
        raise TimeoutError("timed out after 6 minutes")
  5. Step 5

    Persist the result

    The completed payload includes `output_url` and `thumbnail_url`. Download the bytes into your own object storage — S3, GCS, Cloudflare R2 — so your product isn't tied to Photta's CDN for long-term rendering.

Errors

400invalid_request_error
The request is malformed or contains invalid parameters.
401authentication_error
The API key is missing, invalid, or expired.
402insufficient_credits_error
Your account does not have enough credits.
404not_found_error
The requested resource does not exist.
409conflict_error
The request conflicts with current state — e.g. your concurrent-batch cap is reached.
429rate_limit_error
Too many requests. Slow down and retry.Retry-After: 30
500api_error
An internal server error occurred. Retry later.
try:
    photta("POST", "/tryon/apparel", json={
      "product_type": "top",
      "product_images": [
        "https://cdn.yourstore.com/products/shirt-front.jpg"
      ],
      "product_back_image": "https://cdn.yourstore.com/products/shirt-back.jpg",
      "product_category": "t_shirt",
      "mannequin_id": "mnk_f3a9b1",
      "pose_id": "pose_c2d4e5",
      "background": "https://cdn.yourstore.com/backgrounds/studio-loft.jpg",
      "lock_background": True,
      "resolution": "2K",
      "aspect_ratio": "3:4",
      "body_size": "M",
      "expression": "neutral",
      "user_instruction": "Keep the collar pattern crisp and centered."
    })
except PhottaError as e:
    if e.status == 402:
        print("insufficient_credits_error: top up before retrying")
    elif e.status == 429:
        time.sleep(int(e.retry_after or 30))
    else:
        raise

Limits

  • 600 requests per minute per API key (Fixed window; the limit is the same on every plan)
  • A 429 carries Retry-After: 30 seconds
  • Up to 10 MB per image
  • Formats: JPEG, PNG, WebP
  • Concurrent batches per plan: Free 1 · Hobby 2 · Seller 3 · Brand 4 · Studio 6
  • Polling only — the API sends no webhooks.

Why requests is the right pick today

  • No Photta-specific dependency — requests is already in most Python projects
  • Works anywhere Python runs: Django, FastAPI, Flask, Celery workers, Lambda, scripts
  • The client wrapper is about thirty lines — paste it into photta.py and move on
  • Pairs with asyncio via httpx when you need parallel submission

What it does not do

  • No official Photta Python package yet — REST with requests is the documented pattern
  • Polling is the only completion signal; the API sends no webhooks
  • Bearer key only; there is no OAuth client-credentials flow

Questions developers ask

Is there an official Photta Python SDK?

Not yet. The supported integration path is the requests library (synchronous) or httpx (async) — both are a thin wrapper away, and the wrapper on this page works for every capability.

What Python versions are supported?

Python 3.9 and later. The wrapper uses type hints, f-strings and `str | None` syntax, which is fine on 3.10+. On 3.9 swap `str | None` for `Optional[str]`. requests itself officially supports 3.8+, but 3.9+ is the practical minimum for modern Photta projects.

Should I use requests or httpx?

requests if you're making a handful of sequential calls — it's simpler and already in most codebases. httpx if you need async concurrency (parallel submits, bulk backfills) or HTTP/2. The wrapper is tiny either way; swap `requests.request` for `httpx.Client().request` and the rest of the code stays identical.

Can I call it from Django?

Yes. Put the wrapper in `services/photta.py`, import it from a view or a Celery task. Don't call the API inside a synchronous request handler if the job is long — submit from the view, return the generation ID, and poll from a background task or a Channels consumer.

How do I poll from a Celery worker?

Submit the job from one task, then schedule a follow-up task (via `apply_async(countdown=3)`) that polls once and re-schedules itself until the job completes or a max-retries threshold hits. This keeps workers free and avoids long-running tasks that upset autoscalers.

How does the API signal errors?

Non-2xx responses carry a JSON body with an `error` object: `type`, `code`, `message`, plus `param` on 400s and `retry_after` on 429s. The wrapper on this page raises a `PhottaError` carrying all of these so your callers can branch on status code without parsing the body themselves.

Try the API in five minutes

Generate a Photta API key, paste the cURL snippet above, and you'll have your first on-model image before the coffee finishes brewing.

  • 1.5–4 min
  • 5 credits at 2K · 6 credits at 4K
  • async, polling
  • 600 requests/min
  • Bearer key