Build · Django
Render try-on images from a Django worker
In one sentence
A Django integration stores one row per job, submits from a Celery task, polls every 3 seconds until the status is completed, then downloads the file into your own storage.
- Credits: 5 credits at 2K · 6 credits at 4K
- Typical time: 1.5–4 min
- Processing: async, polling
Every figure on this page comes from the Photta API docs · checked
A model that mirrors the job, a Celery task that submits and polls, and storage that keeps the file after the delivery URL is gone.
RESPONSE=$(curl -s -X POST https://api.photta.app/api/v1/tryon/apparel \
-H "Authorization: Bearer $PHOTTA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"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."
}')
ID=$(echo "$RESPONSE" | jq -r '.data.id')const res = await fetch("https://api.photta.app/api/v1/tryon/apparel", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
"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."
}),
});
const { data } = await res.json(); // data.id, data.statusimport 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"]How it works
One row, one task, one file
Model the job, submit from a task, poll with backoff, persist the file, expose the status.
Step 1
Model the job
One row per generation: the product, the generation id, the status and the output URL. The row is what your views read; the API is what the worker reads.
Step 2
Get a Photta API key
Read it from the environment in settings, never from the database, and use a separate key per environment so a staging leak cannot spend production credits.
Step 3
Submit in a task
Views should return immediately. The Celery task POSTs the job and writes the returned id onto the row.
Step 4
Poll with backoff
Poll every 3 seconds until the status is completed or failed. On a 429, wait the Retry-After seconds instead of retrying straight away.
Step 5
Persist the file
Download the finished image into your own storage. The URL delivers one job; your bucket is what serves the product page for years.
Step 6
Expose the status
Your front end polls your row, not the Photta API. That keeps the key on the server and lets you show progress however you like.
Model, task, errors, storage
The submit-and-poll and the error handling are rendered from the API documentation.
# store/models.py
from django.db import models
class TryOn(models.Model):
product = models.ForeignKey("Product", on_delete=models.CASCADE)
generation_id = models.CharField(max_length=64, db_index=True)
status = models.CharField(max_length=16, default="processing")
output_url = models.URLField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)# store/tasks.py
from celery import shared_task
@shared_task(bind=True, max_retries=3)
def run_try_on(self, try_on_id):
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")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# store/tasks.py — the output URL delivers one job; your storage keeps it
import requests
from django.core.files.base import ContentFile
def persist(try_on, output_url):
body = requests.get(output_url, timeout=60).content
try_on.product.gallery.save(f"{try_on.generation_id}.jpg", ContentFile(body))
try_on.status, try_on.output_url = "completed", output_url
try_on.save(update_fields=["status", "output_url"])Questions developers ask
Questions Django developers ask
Why a worker instead of a view?
A generation takes longer than a request should. A worker survives a closed browser tab, a deploy and a timeout; a view survives none of them.
How long should the task poll?
Long enough for the endpoint’s documented latency, with a ceiling. The samples here stop after a fixed number of attempts rather than polling forever.
Can I use async Django instead of Celery?
Yes — the calls are plain HTTP. What matters is that the work outlives the request, whichever way you arrange it.
How do I test without spending credits?
Record one real response and replay it in tests. The resource endpoints and the detail photo analysis are free, so key and connectivity checks cost nothing.
Can one key serve every environment?
It can, but every key spends the same balance, so a staging loop drains production credits. Separate keys make the damage visible and easy to stop.
Related pages
Related reading
Capabilities
Virtual try-on API for apparel
REST API that places apparel on AI mannequins. Asynchronous, 1.5–4 minute generation, 5 credits at 2K, 6 at 4K. cURL, Node.js and Python examples.
ReadQuickstart
First Photta image in under five minutes.
Sign up, generate an API key, run your first cURL call, poll until it completes, persist the result. Five steps, documented end-to-end.
ReadLanguages
Python walkthrough
ReadErrors
Read
Every figure on this page comes from the Photta API docs · checked
A worker, a row and a file
The submit-and-poll on this page is rendered from the API documentation.