Build · Shopify
Build a Shopify try-on app
In one sentence
A Shopify try-on app reads the product image from the Admin API, POSTs it to Photta, polls the job in a background worker, then hands Shopify the finished URL through productCreateMedia.
- 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
Read the product photo from the Admin API, render the try-on with Photta, and push the result back as product media. Four calls, no image handling of your own.
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
From a product page to a try-on image
Scope the app, read the photo, submit, poll off the request path, attach the media.
Step 1
Scope the app
A try-on app needs to read products and write media. Ask for the product scopes your app actually uses and nothing else — merchants read the consent screen.
Step 2
Get a Photta API key
One key per environment, kept in your app’s server-side config. It never travels to the storefront or to the merchant’s browser.
Step 3
Read the product image
Query the product for its first image. That URL is public, which is exactly what the Photta endpoint wants — no re-upload, no signed URL to manage.
Step 4
Submit the try-on
POST the image with a mannequin and a pose. The response is immediate and carries the generation id; nothing is rendered yet.
Step 5
Poll in a background job
Shopify expects a webhook handler to answer in seconds, and a generation takes longer than that. Acknowledge, enqueue, and poll every 3 seconds from the worker.
Step 6
Attach the media
Hand Shopify the finished URL and it downloads the file itself. Persist the image on your side too, so the gallery survives independently.
The four calls, end to end
The Photta request and poll are rendered from the API documentation; the Shopify calls are the Admin API’s own.
// Shopify Admin GraphQL — the product's first image is your source photo
const query = `query($id: ID!) {
product(id: $id) { title images(first: 1) { nodes { url } } }
}`;
const res = await admin.graphql(query, { variables: { id: productGid } });
const { data } = await res.json();
const productImage = data.product.images.nodes[0].url;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.statusasync function waitFor(id) {
for (let attempt = 0; attempt < 120; attempt++) {
const res = await fetch(`https://api.photta.app/api/v1/tryon/apparel/${id}`, { headers: { Authorization: `Bearer ${process.env.PHOTTA_API_KEY}` } });
const { data } = await res.json();
if (data.status === "completed") return data.output_url;
if (data.status === "failed") throw new Error(data.error_message ?? "generation failed");
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error("timed out after 6 minutes");
}// Shopify downloads the URL itself, so hand it the Photta output_url
const mutation = `mutation($id: ID!, $media: [CreateMediaInput!]!) {
productCreateMedia(productId: $id, media: $media) {
mediaUserErrors { field message }
}
}`;
await admin.graphql(mutation, {
variables: {
id: productGid,
media: [{ originalSource: outputUrl, mediaContentType: "IMAGE", alt: "Virtual try-on" }],
},
});Questions developers ask
Questions Shopify developers ask
Where does the Photta key live in a Shopify app?
In your app server’s environment, never in the theme, an app block or app proxy response. Anything the storefront can read, a shopper can read.
Which Shopify scopes does this need?
Reading the product image and writing product media. Photta needs no Shopify access at all — your app is the only thing that talks to both sides.
How do I keep the webhook fast?
Acknowledge immediately, enqueue the work, and poll from the worker. A generation takes longer than a webhook handler should, so nothing that waits on it belongs in the handler.
Can I bill merchants per image?
That is your pricing decision. Photta charges your account in credits per call, so the per-image cost is known before you render; what you charge on top is up to you.
What if a product has no photo?
Skip it rather than sending a placeholder. The endpoint needs a real garment photograph, and a placeholder spends credits on an image no one will publish.
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
Node.js walkthrough
ReadRate limits
Read
Every figure on this page comes from the Photta API docs · checked
Ship the app, not the plumbing
The Photta calls on this page come straight from the API documentation. Get a key and the first render is minutes away.