What changed, when, and what it means for an integration.
Dated entries, newest first. Anything that would break an existing integration is called out as Breaking with what to do about it.
This is a young product and the entries say so. Where something was shipped wrong and corrected, the correction is listed rather than quietly folded into the next release — a changelog that only records additions is not much use for deciding whether to upgrade.
Read this one if you run a storefront. The hourly job that deletes expired shopper photographs — /api/maintenance/purge — had never executed, from the day it was written until today. Vercel Cron issues GET; the route exported only POST, so every scheduled run answered 405. Everything an operator would check looked correct: a green deploy, a correct schedule entry, the job listed, and the route working perfectly when called by hand.
No photograph outlived its window. We checked the table before fixing it: two person images existed and neither had reached its expires_at, so nothing was retained past the promise. That is luck rather than design, and the marketing site had been saying "shopper photos auto-delete" throughout. The fix is a GET alias, the same one the health job had carried since it was written, and both are now covered by a cross-check where every scheduled job watches the others.
Added — erasure over the API. DELETE /api/tryon/v1/customers/:externalUserId removes a shopper's photograph, the result images made from it, and the identifiers tying records to them, and returns a receipt saying what went. GET on the same path shows what is held and the consent trail. It does not remove that a generation happened or what it cost — that is your billing record, not the subject's personal data. Previously a subject request meant writing to us and waiting for a person; that path still exists.
Added — outbound webhooks. Signed POST on generation.completed and generation.failed, configured under Settings → Webhooks. Verify with the X-Webhook-Signature header: HMAC SHA-256 over the raw body. Hash the bytes you received — re-encoding the parsed object changes them and the signature will never match. Retries run at 30s, 2m, 10m, 1h and 6h, then the delivery is given up.
Corrected — the AI disclosure now exists. The 2026-09-07 entry below says results carry no composited badge and no provenance metadata, and that "none of that is implemented". That was true when written and is no longer.
Two mechanisms, and they differ in whether you can turn them off. Provenance metadata is written into every result, always, and takes no flag — there is no setting for it, here or anywhere. The visible AI MODIFIED badge is composited by default and is configurable: per account under Settings → Workspace, or per request with output.aiLabel: false on the server API. The widget API does not accept the override, because it runs on a publishable key that ships in your page source and anyone reading it could otherwise strip the disclosure.
Both are recorded from what the compositor actually did rather than what it was asked to do, so a badge that failed to render is not recorded as applied. Treat any result generated before 2026-09-13 as unlabelled, whatever an older record says.
Still not C2PA, and the distinction is deliberate. What we write is an unsigned text field naming the model and marking the image machine-generated. C2PA is a signed manifest bound to a certificate a verifier can trace. Ours is neither. Anyone can strip or forge it. It is worth having and it is not proof — this is the second time this page has had to be precise about that, having once claimed more than was built.
Added — garment screening. Uploaded garment images are now screened before they reach a generation model, on a narrower set of categories than a person photograph. sexual is deliberately excluded: automated moderation flags lingerie and swimwear product shots, and those are categories this product exists to serve.
Changed — pricing. The site advertised four subscription tiers (FREE / STARTER / GROWTH / PRO) with monthly allowances and a "Remove branding" feature. None of them could be bought; there is no plan column and nothing bills monthly. The product sells credit packs, one try-on spends one credit, failed generations are never charged, and credits do not expire. The pages now show what your operator actually charges.
Added in the console. CSV export on Products and Logs honouring the filters on screen; filters and a detail view on Logs; paging on the credit ledger; and per-store sync exclusions so a synced catalogue can leave products out.
Added — the server Try-On API. /api/tryon/v1/ with POST /try-on, GET /generations/:id and GET /account/credits, authenticated by a secret key. The API reference previously documented endpoints that had never existed in any version; that page was rewritten to describe what is served, with the unbuilt parts listed as unbuilt rather than removed quietly.
Corrected — what a generation costs. Provider cost was recorded from a flat per-call constant rather than from what the provider billed. A two-call generation was recorded as $0.08 against roughly $0.25 actually billed — about 3x understated per generation, and about 6x on the compose call alone, which is where the money is. Every margin figure derived from it was wrong. Cost is now priced from the token counts in the provider's own response, and a generation that cannot be priced honestly is marked estimated rather than given a plausible number.
Corrected — `ai_label_applied`. The column defaulted to true, so historical rows claimed a label that was never composited. It now records what actually happened.
Changed — default model. New generations run on gpt-image-2.5-sunburst. Measured on one fixture it was the fastest of the pool at 45.1s against gpt-image-1's 55.9s, and no worse to look at. That is a change of default, not a quality gate result — the gate has still not been scored.
Security. Provider API keys are encrypted at rest; the database no longer holds one in the clear. Database functions were revoked from the anon and authenticated roles, so every call goes through the server rather than being reachable from a browser with a publishable key.
Breaking, for anyone calling the storefront API directly. POST /api/storefront/v1/try-on now requires a consent object and refuses the request without one:
{ "person": "data:image/jpeg;base64,…",
"productIds": ["SKU-1234"],
"consent": { "version": 1, "at": "2026-09-08T03:25:56.329Z" } }A missing or malformed object returns 400 CONSENT_REQUIRED, and the check runs before the photograph is decoded — refusing afterwards would mean the image had already been read.
You do not need to change anything if you use the widget: it has sent this since 2026-09-07 and will not open a file picker without it. This affects custom upload flows only, which the previous entry already identified as carrying the obligation themselves.
Added. The agreement is stored on the generation record — consent_version and consent_at — so it survives the shopper clearing their browser and is timestamped by us rather than by them. A client clock running ahead is clamped to now rather than rejected: the agreement happened, only the clock is wrong, and a record of consent given in the future reads as tampering.
Two limits stated plainly, because an audit record nobody can rely on is worse than none. It records an assertion by the caller, not proof — a custom integration can send anything. And the obligation still sits with the merchant, who is the controller. What it buys is that the step cannot be skipped in silence.
Added. /privacy and /terms. The operator's legal identity is configuration (LEGAL_* environment variables); until it is set, both pages carry a visible notice and are served noindex.
Added. The storefront widget now requires a shopper to agree before it will open a file picker. Consent is remembered per browser, shows the date it was given, and can be withdrawn from the same panel. It is not configurable — a merchant cannot switch it off.
Breaking for anyone embedding a custom upload flow. If you built your own UI against the storefront API rather than using the widget, you are responsible for collecting consent yourself. The endpoint accepts a consent object; it is not yet stored, so today the obligation is entirely yours.
Corrected. The documentation stated that every generated image carries C2PA-style provenance metadata and a composited AI MODIFIED badge, and that the metadata was not configurable. None of that is implemented. The ai_label_applied column defaults to true, so historical generation records assert a label that was never applied — treat that column as unreliable for any period before compositing actually ships.
Corrected. The documentation described DELETE /customers/:id for erasure. That endpoint has never existed, and neither has the /api/tryon/v1 namespace around it. If you integrated against it, nothing was deleted. Erasure is manual today.
Corrected. The marketing site claimed a try-on completes "in under 10 seconds". The measured average is about 50 seconds. The copy now says so.
Added. The storefront widget: one script tag, shadow DOM, no build step. It runs unchanged on a pasted snippet, a Shopify theme app extension and a WooCommerce install.
Added. The storefront API — POST /try-on, POST /sessions, GET /generations/:id — with two authentication modes: a publishable key bound to an origin allowlist, or a short-lived session token for platforms that can prove the request came through their storefront.
Added. Shopify app with OAuth install, catalogue sync and four webhooks. On Shopify the page carries no key at all: requests go through Shopify's app proxy.
Added. WooCommerce plugin using one-time install codes instead of a pasted key. The HTTP contract is verified; the PHP itself has not yet been run against a live WordPress install.
Added. API keys with origin allowlisting. A publishable key with no origins is refused at creation and at resolution. Only a hash is stored and the plaintext is shown once.
Fixed. The try-on dialog was being hidden on Shopify's Dawn theme, which ships div:empty { display: none }. Everything the widget renders lives in a shadow root, so the host element genuinely is empty in the light DOM.
Added. Photo screening before any generation model runs, failing closed — including when the screening service itself is unavailable.
Added. Person-image storage with real retention: a private bucket, ten-minute signed URLs, an expiry date in the storage path and an hourly sweep that deletes bytes before marking rows.
Added. Layered rate limits — per shopper, per staff user, per account, per platform — configurable in the UI, where the host's values are both the default and the ceiling.
Added. Credit purchase through Stripe (USD) and Razorpay (INR), both over plain REST. Money is reported per currency and never summed across them.
Added. Team invites, an account switcher, and roles that actually restrict spending and billing access.
Added. Email confirmation, password reset and Google sign-in, with the app owning the OAuth handshake rather than the auth host.
Fixed — security. /api/generate took the account id from the request body and never checked a session, so a POST naming any account could spend that account's credits. It now reads the account from the session cookie.
Fixed — security. Generation had no credit check and no floor on the balance, so an account at zero could generate indefinitely while its balance went negative.
Changed. The signup bonus dropped from 25 credits to 5. Real inference costs about $0.04 a call, so 25 was giving away $1–2 per signup.
Added. The GenFashion brand, a revenue estimator, a platforms page, a blog, and booking and contact routes.
Changed. /how-it-works folded into the landing page. Old links redirect to the anchor.
Changed. The product name moved behind BRAND_NAME, covering everywhere a host client's customer meets it — the checkout line item, support email, page titles.
Added. The admin platform, the try-on playground, the documentation site, the AI assistant with human escalation, and the data access layer: around 100 Postgres functions over PostgREST, with no inline queries and no Supabase SDK anywhere.