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')

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.status
async 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.

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.