Skip to content

Product Shot ​

Product Shot turns a plain product photo into a catalogue or lifestyle image. FOTOhub AI cuts the product out, builds the background (a clean studio colour, or a generated scene), places the product with a believable shadow and composes the result. The product's own pixels are kept: labels, logos and text on the product stay legible, because the product is composited from your original photo, not redrawn. Every result passes an automatic quality check before you are charged for it.

Capabilities

  • Presets: studio white (marketplace-compliant main image), grey, black, five lifestyle scenes, four seasonal scenes, or your own scene prompt
  • Standard and Premium scene tiers; Premium lets you pick the scene model
  • Outputs at 1:1 (2000x2000), 4:5 (1600x2000) and 16:9 (2560x1440) from one request
  • Batches for whole catalogues, with per-image charge and refund, retry of failed images, and a zip of the results
  • Price shown first: quote answers the per-image and total price before you commit
  • Failed means not charged: an image that fails is refunded, individually

Availability

Product Shot is rolled out in stages. Until it is enabled for your account, every route answers 403 with {"error": "feature_disabled", "feature": "product_shot"}. Nothing is charged.

Endpoints ​

MethodPathChargedDescription
GET/v1/aiwave/product-shot/confignoPresets, aspects, marketplaces, premium scene models, your saved presets, the consent text (residency), limits
POST/v1/aiwave/product-shot/quotenoPrice per image and in total, availability, ETA
POST/v1/aiwave/product-shot/jobsyesOne job per requested aspect
GET/v1/aiwave/product-shot/jobsnoYour recent jobs (filter by request_id)
GET/v1/aiwave/product-shot/jobs/{job_id}noStatus, stage, result or error, refund state
POST/v1/aiwave/product-shot/jobs/{job_id}/cancelnoCancel; refunded once
GET/v1/aiwave/product-shot/presetsnoYour saved presets
POST/v1/aiwave/product-shot/presetsnoSave a preset
DELETE/v1/aiwave/product-shot/presets/{preset_id}noDelete a saved preset
POST/v1/aiwave/product-shot/batches/quotenoPrice, ceilings and fit of a batch
POST/v1/aiwave/product-shot/batchesper imageCreate a batch
GET/v1/aiwave/product-shot/batchesnoYour recent batches and ceilings
GET/v1/aiwave/product-shot/batches/{batch_id}noStatus, counts, progress, spend, pause, ETA
GET/v1/aiwave/product-shot/batches/{batch_id}/itemsnoPer-image status, result, charge and refund
POST/v1/aiwave/product-shot/batches/{batch_id}/cancelnoStop: waiting images never charged, running ones refunded
POST/v1/aiwave/product-shot/batches/{batch_id}/retry-failedper imageQueue the failed images again
POST/v1/aiwave/product-shot/batches/{batch_id}/resumeper imageResume a paused batch
POST / GET/v1/aiwave/product-shot/batches/{batch_id}/archivenoBuild / fetch the results zip

Base URL: https://apis.fotohub.appAuthentication: Authorization: Bearer fh_live_your_api_key (see Authentication)

Pricing ​

Prices are not hard-coded in this page. They come from the API:

  • quote returns per_image (credits and USD, plus the scene leg for a generated scene) and total for the number of aspects you asked for, for your account.
  • The USD wallet rates are listed by GET /v1/pricing. The price keys are aiw-productshot-std (a Standard image) and aiw-productshot-premium-fee (the composite fee that Premium adds to the scene model's own catalogue price): GET /v1/pricing?model=aiw-productshot-std. While the feature is disabled, GET /v1/pricing?model=aiw-* answers 404 for its keys.
  • A Standard image is priced by the scene backend that will actually run, which the quote names. If an option cannot be sold at a sustainable price, the quote answers available: false with a reason instead.

Two steps of Product Shot run in the United States. Background removal runs on FOTOhub AI servers (Google Cloud, us-central1), and the image review (a quality check) sends a reduced-size (768 px) preview of each finished image to an AI vision model hosted in the United States. Your product photo is sent there for processing only, and the finished images are stored in the EU (FOTOhub storage, Poland). Generated backgrounds are created by FOTOhub AI image models, which may also run outside the EU (in the EU, Switzerland or the United States, or at a partner provider).

Because of that, creating jobs requires an explicit consent flag. GET /config returns it as residency:

json
{
  "consent_version": "2026-10-05",
  "cutout":  {"region": "US", "location": "Google Cloud us-central1 (United States)"},
  "review":  {"region": "US", "location": "AI vision model hosted in the United States"},
  "storage": {"region": "EU", "location": "Azure Poland Central"},
  "text": "Two steps of Product Shot run in the United States ..."
}

Show text to whoever decides, then send consent_us_processing: true and consent_version (the value from the config) with POST /jobs and POST /batches. Without it the API answers 422 consent_required and charges nothing. If you resell Product Shot, pass the notice on to your users and say in your privacy policy that cut-outs and image reviews are processed in the United States. The consent version changed to 2026-10-05 when the image review was added to the notice; send the current value from GET /config, a stale one answers 422 consent_required.

StepWhere it runs
Background removal (cut-out)United States (Google Cloud us-central1)
Image review (768 px preview)United States (AI vision model hosted in the United States)
Background generationFOTOhub AI image models (EU, Switzerland or United States) or a partner provider
Storage of photos and resultsEU (FOTOhub storage, Poland)

Create images ​

python
from fotohub import FotoHub

client = FotoHub()  # reads FOTOHUB_API_KEY

cfg = client.product_shot.config()
consent = cfg["residency"]["consent_version"]

quote = client.product_shot.quote(
    "photos/<your-user-id>/shoe.jpg", preset_id="studio_white", aspects=["1:1", "4:5"]
)
print(quote["total"], quote["available"])

created = client.product_shot.create(
    "photos/<your-user-id>/shoe.jpg",
    preset_id="studio_white",
    aspects=["1:1", "4:5"],
    consent_us_processing=True,
    consent_version=consent,
)
for j in created["jobs"]:
    done = client.product_shot.wait_for_job(j["job_id"])
    print(j["aspect"], done["result"]["outputs"]["delivery"]["url"])
typescript
import { FotoHub } from "fotohub";

const client = new FotoHub({ apiKey: process.env.FOTOHUB_API_KEY! });

const cfg = (await client.productShot.config()) as any;
const created = (await client.productShot.create({
  imagePath: "photos/<your-user-id>/shoe.jpg",
  presetId: "studio_white",
  aspects: ["1:1", "4:5"],
  consentUsProcessing: true,
  consentVersion: cfg.residency.consent_version,
})) as any;
for (const j of created.jobs) {
  const done = await client.productShot.waitForJob(j.job_id);
  console.log(j.aspect, (done.result as any).outputs.delivery.url);
}
bash
curl https://apis.fotohub.app/v1/aiwave/product-shot/jobs \
  -H "Authorization: Bearer $FOTOHUB_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "image_path": "photos/<your-user-id>/shoe.jpg",
    "preset_id": "studio_white",
    "aspects": ["1:1", "4:5"],
    "consent_us_processing": true,
    "consent_version": "2026-10-05",
    "request_id": "6f1d3c52-8a0e-4b49-9c1f-0a2b3c4d5e6f"
  }'
FieldTypeDescription
image_pathstringRequired. Your product photo in your own storage: photos/<your user id>/.... Anything else is a 422
preset_idstringA built-in id (studio_white, studio_grey, studio_black, lifestyle_marble, lifestyle_wood, lifestyle_concrete, lifestyle_bathroom, lifestyle_nature, seasonal_christmas, seasonal_summer, seasonal_autumn, seasonal_spring) or a saved user:<uuid> preset. config().presets is the authoritative list
promptstringA custom scene instead of a preset: 3-400 characters
paramsobjectOverrides: fill (0.3-0.95), anchor (center, bottom), ground_y (0.5-0.97), shadow_mode (none, contact, drop), shadow_opacity (0-1), shadow_direction (-80 to 80), harmonize (bool), background (#RRGGBB)
aspectsstring[]Subset of 1:1, 4:5, 16:9; up to 3; default ["1:1"]. One job, and one charge, per aspect
tier"standard" | "premium"Default standard. Premium applies to generated scenes only
scene_modelstringPremium only: one of config().premium_models
seedintegerReproducible scene
consent_us_processing, consent_versionbool, stringRequired, see above
request_idUUIDIdempotency key: the same request_id returns the jobs it already created (replayed: true) and never bills twice. The SDKs generate one

The request is all or nothing: if the second job cannot be charged, the first is cancelled and refunded.

Answer: {"request_id", "jobs": [{"job_id", "aspect", "status", "replayed"}], "quote": {"per_image", "images", "total"}, "eta", "residency"}.

Result ​

A succeeded job's result is:

json
{
  "outputs": {
    "master":   {"url": "...", "path": "photos/...", "width": 2000, "height": 2000, "bytes": 0, "mime": "image/png"},
    "delivery": {"url": "...", "path": "photos/...", "width": 2000, "height": 2000, "bytes": 0, "mime": "image/jpeg"},
    "preview":  {"url": "...", "path": "photos/...", "width": 0, "height": 0, "bytes": 0, "mime": "image/jpeg"}
  },
  "aspect": "1:1", "preset_id": "studio_white", "tier": "standard",
  "qa": {"passed": true, "reasons": [], "warnings": []},
  "compliance": {"...": "marketplace main-image checks, for presets that have a marketplace"},
  "metrics": {"ssim_product": 0.99},
  "residency": {"cutout": "US", "storage": "EU"}
}

studio_white produces a marketplace main image (pure white background, the product filling about 85% of the frame, 2000 px, sRGB). compliance reports the checks. The values above are illustrative; read the real ones from the response.

Job states and refunds ​

status is queued, running, succeeded, failed or cancelled. A failed job has error: {"code", "message"} (cutout_unavailable, scene_unavailable, scene_refused, composite_failed, qa_failed, ...) and is refunded exactly once; refunded and refund_message tell you it happened. POST .../cancel refunds a queued or running job.

Presets ​

bash
curl https://apis.fotohub.app/v1/aiwave/product-shot/presets \
  -H "Authorization: Bearer $FOTOHUB_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Oak table, golden hour", "prompt": "rustic oak table, warm golden-hour light", "kind": "lifestyle"}'

Saved presets are free and appear in config().user_presets; use the returned id as preset_id.

Batches ​

A batch processes a whole catalogue: up to the ceiling in batches/quote (and GET /batches → ceilings) per batch. Each image is charged when it starts and refunded on its own if it fails.

python
items = [
    {"image_path": "photos/<your-user-id>/a.jpg", "sku": "SKU-1"},
    {"image_url": "https://shop.example.com/b.jpg", "sku": "SKU-2"},   # https only
]
q = client.product_shot.batches.quote(len(items), preset_id="studio_white")
print(q)

batch = client.product_shot.batches.create(
    items, name="Autumn range", preset_id="studio_white",
    consent_us_processing=True, consent_version=consent,
)
final = client.product_shot.batches.wait(batch["batch_id"], on_progress=print)
if final["counts"].get("failed"):
    client.product_shot.batches.retry_failed(batch["batch_id"])
typescript
const batch = (await client.productShot.batches.create({
  items: [
    { image_path: "photos/<your-user-id>/a.jpg", sku: "SKU-1" },
    { image_url: "https://shop.example.com/b.jpg", sku: "SKU-2" },
  ],
  name: "Autumn range",
  presetId: "studio_white",
  consentUsProcessing: true,
  consentVersion: cfg.residency.consent_version,
})) as any;
const final = await client.productShot.batches.wait(batch.batch_id);
  • Quote first. POST /batches/quote with {"skus": N, ...settings} returns the per-image and total price, your ceilings (items per batch, concurrent batches, a daily credit ceiling) and fits (true or false) with the refusals that would stop it (too_many_items, active_batch_limit, daily_cap, unavailable).
  • Items. image_path (your storage) or image_url (https:// only), plus optional sku, external_id and title.
  • Status. active, paused, completed, completed_with_errors, failed, cancelled. A paused batch carries pause.reason (insufficient_funds, daily_cap, feature_disabled, price_changed, not_offered) and a message; fix the cause and POST .../resume (send {"accept_price": true} for price_changed). A daily_cap pause continues by itself after midnight UTC.
  • Failures. GET .../items?status=failed lists them with their error; POST .../retry-failed queues them again.
  • Cancel. Waiting images are never charged; images in flight are refunded.
  • Download. When the batch has finished, POST .../archive builds a zip and GET .../archive returns its parts (409 archive_not_ready while it is being built).
  • Idempotency. request_id again returns the same batch (replayed: true).

Errors ​

StatuserrorMeaning
401authentication_requiredNo or invalid key
402insufficient_fundsWallet too low (required_usd, balance_usd, shortfall_usd, topup_url)
403feature_disabledNot enabled for your account yet
404job_not_found, batch_not_found, preset_not_foundNot yours or gone
409not_cancellable, archive_not_ready, price_changed, not_paused, batch_cancelledState conflict
422invalid_image_path, invalid_preset, premium_needs_scene, invalid_scene_model, consent_required, too_many_items, invalid_image_url, missing_image, no_itemsFixable input; nothing charged
429ceiling_exceeded, active_batch_limit, daily_capConcurrency / daily ceilings; Retry-After; nothing charged
503backend_unavailable, store_unavailable, scene_below_margin_gateNot offered right now; Retry-After unless the option is not sold at all; not charged