Product Optimizations API

Last updated: August 20, 2026

ENTITLEMENT REQUIRED

Generate AI-optimized product descriptions from review data and publish them to Shopify. The optimization workflow is: start → poll → edit (optional) → approve → publish.

Availability: all endpoints in this section require the product_pages entitlement. Without it, write calls return 403 entitlement_required and reads return 200 with content: null. Current plans (Unlimited, Studio) include it. On legacy plans (no longer sold), it is available on Growth and above, and on legacy Pro and Enterprise; stores on the legacy Starter plan do not have it. A store with no active subscription — never subscribed, or cancelled — is in the free state and holds none of the gated entitlements: entitlement-gated endpoints return 403 entitlement_required, and every LLM-backed endpoint returns 402 subscription_required. See Pricing.

Note: Product IDs in these endpoints are internal BetterReviews IDs, not Shopify IDs. Use GET /products/lookup/:platform_id to convert a Shopify product ID first.

Start Optimization

POST /api/v1/products/:product_id/optimize

Starts an async AI optimization job. Returns immediately with status pending.

Request

{
  "feedback": "Focus on fabric quality and comfort",
  "blocksToOptimize": ["description", "features"]
}
FieldTypeRequiredDescription
feedbackstringNoInstructions for the AI (tone, focus areas, keywords)
blocksToOptimizestring[]NoSpecific sections to optimize

Response (202)

{
  "id": "uuid",
  "status": "pending",
  "version": 1,
  "created_at": "2026-03-27T10:00:00Z"
}

429 unpublished_optimization_limit — you've reached the per-period maximum of products optimized-but-not-published. Applies on stores with credit-based publishing enabled. Publish a draft to unlock the next, or wait for the next billing period. Re-optimizing a product you've already drafted this period is exempt.


Get Current Optimization

GET /api/v1/products/:product_id/optimization

Poll this endpoint after starting an optimization to check status and get results.

Response (200)

{
  "id": "uuid",
  "version": 3,
  "status": "draft",
  "content": {
    "description": "...",
    "features": ["..."],
    "seo_title": "...",
    "seo_description": "..."
  },
  "created_at": "2026-03-27T10:00:00Z",
  "published_at": null,
  "error_message": null
}

List All Versions

GET /api/v1/products/:product_id/optimizations?limit=10&offset=0

Returns paginated list of all optimization versions for a product.


Edit Optimization

PATCH /api/v1/products/:product_id/optimization

Request

{
  "content": {
    "description": "Updated description text..."
  }
}

Regenerate

POST /api/v1/products/:product_id/optimization/regenerate

Creates a new version. Accepts same parameters as Start (feedback, blocksToOptimize).


Approve

POST /api/v1/products/:product_id/optimization/approve

Marks the optimization as approved and ready to publish.


Publish to Shopify

POST /api/v1/products/:product_id/optimization/publish

Writes the optimization content to the Shopify product metafield. The storefront updates with the new content.

Cost: generating and regenerating drafts are always free. Publishing draws 5 AI credits per distinct product, per billing period (re-publishing the same product in the same period is free) only on stores with credit-based publishing enabled, a per-store setting that is off by default on every plan. With it off, publishing draws nothing.

402 credit_limit_reached — returned on publish when credit-based publishing is enabled for the store and its credit pool for the period is exhausted. Enable auto-recharge and extra credits bill at the plan's block rate; with auto-recharge active, publish proceeds and bills the overage. Stores with credit-based publishing off never receive this error.


Discard

DELETE /api/v1/products/:product_id/optimization


Optimization Statuses

StatusMeaning
pendingProcessing (async job running)
draftGenerated, ready for review
approvedMerchant approved, ready to publish
publishedLive on Shopify storefront
failedGeneration failed (check error_message)
discardedMerchant discarded