Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/VisualGraphxLLC/API-HUB/llms.txt

Use this file to discover all available pages before exploring further.

Markup rules are the pricing layer that sits between a supplier’s wholesale cost and the price displayed on an OnPrintShop storefront. For every product a customer pushes, API-HUB evaluates the customer’s rules, selects the best match, and applies the configured percentage, margin floor, and rounding strategy. The resulting prices are embedded in the push payload that n8n sends to OPS.

How rule resolution works

The markup engine (backend/modules/markup/engine.py) evaluates all rules for a customer in a single pass and returns the one rule that most specifically matches the product being priced. Specificity is ordered from most to least:
  1. Product-levelscope = "product:{supplier_sku}". Matches exactly one SKU family.
  2. Category-levelscope = "category:{category}". Matches all products in a category (e.g., "category:T-Shirts").
  3. Globalscope = "all". Customer-wide catch-all.
When multiple rules exist at the same specificity level, the rule with the highest priority value wins. Priority is an integer — there is no upper bound. This lets you layer exceptions cleanly: set the global rule to priority 0, category overrides to 10, and product-specific rules to 100.
If no rule matches a product (the customer has no rules at all, or none cover the product’s SKU or category), the engine returns the supplier’s base price unchanged — no markup is applied.

Markup rule fields

Controls which products the rule applies to. Three formats are accepted:
  • "all" — applies to every product for this customer.
  • "category:T-Shirts" — applies to all products whose category field matches T-Shirts exactly.
  • "product:PC61" — applies to the product whose supplier_sku is PC61 (Port & Company Essential Tee, for example).
The percentage to add on top of the supplier base price. Stored as a NUMERIC(5,2) — for example, 45.00 means a 45% markup. The calculation is base_price × (1 + markup_pct / 100).
An optional minimum margin percentage. If the computed price after markup_pct falls below base_price × (1 + min_margin / 100), the price is raised to the floor. This protects against closeout or sale prices eroding your margin.
Post-markup rounding strategy. Three values:
  • "none" — round to the nearest cent only.
  • "nearest_99" — round down to the nearest whole dollar, then add $0.99 (e.g., $14.23 → $13.99).
  • "nearest_dollar" — round to the nearest whole dollar (standard banker’s rounding).
Integer tiebreaker within a scope level. Higher wins. Defaults to 0. Use higher values for exceptions that should override a base rule at the same scope.

Creating and managing rules

Rules are scoped to a single customer. There is no limit on how many rules you can create for a customer.
GET /api/markup-rules/{customer_id}
Returns all rules for the customer ordered by priority DESC. No authentication header is required for reads (protected at the network level in production).
[
  {
    "id": "c1a2b3c4-...",
    "customer_id": "3fa85f64-...",
    "scope": "all",
    "markup_pct": 45.0,
    "min_margin": 30.0,
    "rounding": "nearest_99",
    "priority": 0,
    "created_at": "2026-04-16T10:00:00Z"
  }
]

Price calculation in detail

The engine applies rules in this exact sequence for every variant in the product:
# 1. Resolve the best-matching rule for this product
rule = resolve_rule(rules, product.supplier_sku, product.category)

# 2. Apply percentage markup
price = base_price * (1 + markup_pct / 100)

# 3. Enforce minimum margin floor (if configured)
floor = base_price * (1 + min_margin / 100)
if price < floor:
    price = floor

# 4. Apply rounding strategy
if rule.rounding == "nearest_99":
    price = floor(price) + 0.99
elif rule.rounding == "nearest_dollar":
    price = round(price)

# 5. Quantize to 2 decimal places (ROUND_HALF_UP)
price = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
All arithmetic uses Python’s Decimal type with str() coercion on float inputs to avoid floating-point drift. The base_price used in markup is the supplier’s wholesale cost stored per variant in product_variants.base_price.

Fetch the computed push payload

After rules are configured, you can inspect exactly what n8n will receive before triggering a push. This endpoint is the same one n8n calls internally.
GET /api/push/{customer_id}/product/{product_id}/payload
X-Ingest-Secret: <INGEST_SHARED_SECRET>
This endpoint requires the X-Ingest-Secret header matching the INGEST_SHARED_SECRET environment variable. It is intended for internal use by n8n and integration testing — not for end-user access.
Example response:
{
  "product": {
    "supplier_sku": "PC61",
    "name": "Port & Company Essential Tee",
    "brand": "Port & Company",
    "category": "T-Shirts"
  },
  "variants": [
    {
      "sku": "PC61-S-White",
      "color": "White",
      "size": "S",
      "base_price": 3.98,
      "final_price": 5.77,
      "inventory": 1200
    }
  ],
  "images": [
    { "url": "https://cdn.sanmar.com/...", "image_type": "front" }
  ],
  "markup_rule": {
    "id": "c1a2b3c4-...",
    "scope": "all",
    "markup_pct": 45.0,
    "priority": 0
  }
}
The final_price on each variant is what gets written to OPS via setProductPrice. The markup_rule block tells you which rule was applied — useful for auditing unexpected prices.

OPS-ready variant bundles and options

Two additional endpoints pre-shape data into the structures OPS mutations expect directly.

Variants bundle

GET /api/push/{customer_id}/product/{product_id}/ops-variants?ops_products_id=0Returns parallel sizes and prices arrays aligned by index, ready to loop over in n8n for setProductSize and setProductPrice mutations. Pass the OPS products_id returned by setProduct as ops_products_id.

Options

GET /api/push/{customer_id}/product/{product_id}/ops-optionsReturns enabled product options shaped for OPS setAdditionalOption mutations. Includes source_master_option_id and per-attribute source_master_attribute_id for push mapping traceback. Both endpoints require X-Ingest-Secret.

Storefront-level pricing overrides

For finer control beyond customer-wide markup rules, product_storefront_configs.pricing_overrides accepts per-product exceptions stored as a JSONB object. These are applied by the push pipeline on top of (or instead of) the markup rule result.
Override keyBehavior
fixed_unit_priceBypasses markup calculation entirely. The value is used as-is for all variants.
extra_markup_pctAdded on top of the markup rule result. Useful for individual products that carry higher fulfillment costs.
nearest_99true / false. Overrides the rule’s rounding strategy with nearest_99 for this product.
nearest_dollartrue / false. Overrides the rule’s rounding strategy with nearest_dollar for this product.
Use fixed_unit_price sparingly — it disconnects the storefront price from the supplier cost entirely, which means cost increases will not be reflected automatically on re-push. Prefer extra_markup_pct when you want a consistent margin above the base.

Build docs developers (and LLMs) love