Pretty Product Options

API Reference · v1

The Pretty Product Options API

Programmatic access to templates, options, pricing, products, customer groups and saved configurations. Available on the Advanced plan.

Authentication

Generate a token in the app under Settings → API access (shown once; only its hash is stored). API access is available on the Advanced plan. Send the token on every request:

Authorization: Bearer cov_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • • Base URL: your app host, e.g. https://<app-host>/api/v1/…
  • • All requests and responses are JSON.
  • • Rate limit: 120 requests/minute per shop429 when exceeded.
  • • Errors: { "error": "…" } with 400 / 401 / 402 / 404 / 405 / 422 / 429 / 500.
  • 401 = bad token or app uninstalled; 402 = plan no longer Advanced.
  • • Regenerating or revoking the token invalidates the old one immediately.

Templates

Templates hold the option structure a product configurator renders: sections → options → choices, plus visibility rules and ruleset versions.

GET/api/v1/templates

List all templates.

Response

{ "templates": [{ "id": "…", "name": "Roller blinds", "product_config_id": null }] }
  • product_config_id set means a per-product option set; null means a shared template.
POST/api/v1/templates

Create a template.

Request body

{ "name": "Roller blinds" }

Response

201 { "ok": true, "id": "…" }
GET/api/v1/templates/{id}

Read the full tree: template, sections → options → choices, visibility rules, and the ruleset version list.

PUT/api/v1/templates/{id}

Update the structure declaratively.

Request body

{
  "name": "Roller blinds",
  "sections": [
    {
      "title": "Size",
      "options": [
        { "key": "width",  "label": "Width (cm)",  "input_type": "number", "required": true, "default_value": "120" },
        { "key": "height", "label": "Height (cm)", "input_type": "number", "required": true, "default_value": "150" }
      ]
    },
    {
      "title": "Fabric",
      "options": [
        {
          "key": "fabric", "label": "Fabric", "input_type": "single_select",
          "required": true, "display_mode": "swatch",
          "choices": [
            { "label": "Blockout White", "value": "blockout_white", "swatch_color": "#f5f5f0" },
            { "label": "Sunscreen 5%",   "value": "sunscreen_5", "price": 15 }
          ]
        }
      ]
    }
  ]
}
  • sections replaces the whole structure declaratively (options and choices get new ids).
  • Visibility rules are cleared when sections is provided — pricing rules are unaffected because conditions reference stable option keys.
  • Omit sections to rename only.
DELETE/api/v1/templates/{id}

Remove the template and everything under it.

Pricing rulesets

Rulesets are versioned. Publishing replaces the live version the storefront prices with.

GET/api/v1/rulesets/{productConfigId}

Read the live ruleset the storefront prices with: rules, validations, version and base price.

PUT/api/v1/rulesets/{productConfigId}

Replace the rules as a new version.

Request body

{
  "rules": [
    { "kind": "adder", "config": { "label": "Base price", "amount": 89 } },
    { "kind": "raw_expression", "config": { "label": "Size", "expression": "(width/100)*(height/100)*65" } },
    {
      "kind": "multiplier", "config": { "label": "Motorised", "factor": 1.25 },
      "applies_when": { "combinator": "and", "conditions": [{ "option_key": "control_type", "operator": "eq", "value": "motorised" }] }
    }
  ],
  "validations": [
    {
      "conditions": { "combinator": "or", "conditions": [{ "option_key": "width", "operator": "lt", "value": 30 }] },
      "action": "block", "block_message": "Minimum width is 30 cm."
    }
  ],
  "publish": true
}
  • Creates a new draft version containing exactly these rules; bad rules fail compilation with 422.
  • validations, when present, replaces the template's validation rules.
  • publish: true makes it live immediately.
  • Rule kinds: adder, multiplier, dimension_lookup, tier, raw_expression.

Price computation

Evaluate any configuration against the live ruleset — the same engine the storefront uses. No signature is issued; signed cart payloads only come from the storefront flow.

POST/api/v1/price

Compute a price for a configuration.

Request body

{
  "product_config_id": "…",
  "selections": { "fabric": "sunscreen_5", "control_type": "motorised" },
  "dimensions": { "width": 120, "height": 150 }
}

Response

{ "price": 262.13, "breakdown": { … }, "validation": { "ok": true }, "ruleset_version": 7 }
  • Batch: { "items": [ …same shape… ] } → { "results": […] }.

Draft orders & cart links

Programmatic assisted selling — the API twins of the in-app draft-order builder. Prices are always recomputed server-side from the live ruleset, and each line's configuration is persisted so orders reconcile normally.

POST/api/v1/draft-orders

Create a Shopify draft order with configured lines — returns the invoice link.

Request body

{
  "items": [
    {
      "product_config_id": "…",
      "selections": { "fabric": "sunscreen_5" },
      "dimensions": { "width": 120, "height": 150 },
      "quantity": 2
    }
  ],
  "email": "customer@example.com"
}

Response

{ "ok": true, "invoice_url": "…" }
  • Prices are locked at creation — the customer opens the link and pays.
  • Items may use shopify_product_id instead of product_config_id.
  • Validation blocks return 422 with the blocking message.
POST/api/v1/cart-links

Same items shape, editable vehicle: a link that rebuilds the items in the customer's cart.

Request body

{ "items": [ …same shape as draft-orders… ] }

Response

{ "ok": true, "url": "https://your-store.com/cart?coverly_cart=…" }
  • Items are re-priced and re-signed when the link is opened — never stale.
  • The customer can edit any line in their cart before checking out.
  • Use draft orders for agreed quotes; cart links for editable proposals.

Products

GET/api/v1/products

List all configured products.

PUT/api/v1/products/{shopifyProductId}

Connect or update a product.

Request body

{ "anchor_variant_id": 45678901234, "template_id": "…", "base_config": { … }, "currency_mode": "auto_convert" }
  • base_config and currency_mode are optional.
DELETE/api/v1/products/{shopifyProductId}

Disconnect the configurator from the product.

Customer groups

GET/api/v1/groups

List customer groups.

POST/api/v1/groups

Create a group keyed to a customer tag.

Request body

{ "name": "Wholesale", "tag": "wholesale" }
DELETE/api/v1/groups?id={uuid}

Delete a group.

Saved configurations

The read model for ERP and manufacturing. Each row has uid (matches the order line's _config_id property), full selections, computed_price, price_breakdown, ruleset_version, lifecycle status (draft | saved | shared | carted | ordered | abandoned) and shopify_customer_id.

GET/api/v1/configs?status=ordered&since=2026-01-01T00:00:00Z&limit=100

Query saved configurations by status and time window.

Typical ERP sync

  1. POST /api/v1/templates, then build the structure with PUT /api/v1/templates/{id}
  2. PUT /api/v1/products/{productId} — connect a product to the template
  3. PUT /api/v1/rulesets/{productConfigId} with publish: true — price it
  4. Nightly: GET /api/v1/configs?status=ordered&since=… — pull specs into manufacturing; recompute spot checks with POST /api/v1/price