GuidesProduct DataImport Products to Your Backend

How to import products into your backend

This guide walks through a backend-agnostic workflow for importing promotional products from PSRESTful into any catalog system: BigCommerce, Salesforce, Odoo, Postgres, MySQL, an in-house ERP, etc. The mechanics of talking to the PSRESTful API are the same regardless of destination; only the final upsert step depends on your backend.

Overview

The import runs in two phases:

  1. Discover: call GET /extra/v2/products with filters (status=active, supplier_code=...) and walk the pagination. Save each result’s extraId.
  2. Hydrate: for every extraId, call GET /extra/v2/products/{extra_id} with expand=medias&expand=combined_ppc&expand=inventory&expand=classifications. A single response now contains everything needed to build a full catalog entry: descriptive fields, variants, images, pricing, stock, and third-party classifications.

This guide stops at the point where you have a fully hydrated product in memory. The mapping into your backend’s schema and the translation layer for categories, locations, and decorations are your responsibility, covered in Step 4 and the translation layer callout.


Prerequisites

  • A PSRESTful API key. See Authentication.
  • Knowledge of your merchant backend’s product, variant, image, price, and inventory schemas.
  • (Optional, Python only) psdomain: Pydantic models for every PSRESTful/PromoStandards payload so you can work with typed objects instead of dicts. Requires Python 3.12+.

Step 1. Discover the products to import

List products with whatever filters match your import policy. The most common starting point is “all active products for one supplier”:

curl -X GET "https://api.psrestful.com/extra/v2/products?status=active&supplier_code=HIT&page_size=100" \
  -H "X-API-Key: your-api-key"

Useful filters for import scenarios

ParameterTypeDescription
statusstringdraft, active, closeout, discontinued
supplier_codestringImport one supplier at a time (e.g. HIT, SanMar)
brandintegerFilter to a single brand
main_categorystringSupplier’s top-level category
is_rush_servicebooleanOnly products with rush service
lead_time__rangestringe.g. 5,10 for 5 to 10 day lead times
list_price__rangestringe.g. 0,25 for items priced ≤ $25
searchstringMatches name, productId, part SKUs, GTINs
orderingstringname, list_price, -lead_time, etc.

Walk the pagination

Save each result’s extraId; you’ll use it in Step 2.

def list_all_products(api_key: str, supplier_code: str):
    url = "https://api.psrestful.com/extra/v2/products"
    params = {"status": "active", "supplier_code": supplier_code, "page_size": 100, "page": 1}
    headers = {"X-API-Key": api_key}
    while True:
        resp = requests.get(url, headers=headers, params=params).json()
        for item in resp["results"]:
            yield item["extraId"]
        if resp.get("next") is None:
            break
        params["page"] += 1

Step 2. Hydrate with full detail via expand

Call the detail endpoint for each extraId and request every expansion you need in a single round-trip:

curl -X GET "https://api.psrestful.com/extra/v2/products/21832?expand=medias&expand=combined_ppc&expand=inventory&expand=classifications&currency=USD" \
  -H "X-API-Key: your-api-key"

Expansion options

ExpansionWhat you getUse for
mediasImages, art templates, and videos per product / partProduct gallery, color swatches, decoration previews
combined_ppcList / net / customer pricing plus decoration pricingPrice lists, customer-specific pricing
inventoryStock quantity per FOB point / warehouseAvailability, future available when supplier supports INV 2.0.0
classificationsGoogle, Shopify categoriesSEO, marketplace feeds, tax configuration

With all four expansions the response contains everything required for a complete catalog entry in one request.


Step 3. Use psdomain instead of raw dicts (Python)

psdomain is the Pydantic model library for PSRESTful/PromoStandards payloads. It replaces hand-written dict access (data["Product"]["ProductPartArray"]["ProductPart"][0]["ColorArray"]["Color"][0]["hex"]) with typed attribute access and validation.

Install:

pip install git+https://github.com/GallardoSolutions/psdomain.git

Key models relevant to an import flow:

PayloadModelImport
Standard Product Data 2.0.0 responseProductResponseV200from psdomain.model.product_data.v_2_0_0 import ProductResponseV200
Media ContentMediaContentfrom psdomain.model.media_content import MediaContent
Configuration & PricingConfigurationAndPricingResponsefrom psdomain.model.ppc import ConfigurationAndPricingResponse
Inventory 2.0.0InventoryLevelsResponseV200from psdomain.model.inventory.v_2_0_0 import InventoryLevelsResponseV200

Parse a Product Data response:

from psdomain.model.product_data.v_2_0_0 import ProductResponseV200
 
resp = ProductResponseV200.model_validate({
    "Product": product_payload,       # from PSRESTful
    "ServiceMessageArray": None,
})
product = resp.Product
 
print(product.productId, product.productName)
print("Colors:", [c.colorName for c in product.available_colors])
print("Sizes:", product.sizes)
for part in product.ProductPartArray.ProductPart:
    print(part.partId, part.ColorArray.Color[0].hex if part.ColorArray else None)

Product exposes convenience properties such as available_colors, sizes, and variants_per_color, which remove most of the boilerplate from an importer.


Step 4. Map to your backend schema

The hydrated payload carries far more information than most backends model natively. Pick the fields that matter for your catalog and drop the rest.

PSRESTful fieldTypical target fieldNotes
supplierCode + productIdSKU or external referenceproductId is not globally unique; always qualify it with supplierCode
productNameTitle
description (list of strings)Body / long descriptionJoin with \n\n
productBrand, lineNameBrand / product line
primaryImageURLHero imageFall back to the first medias entry
ProductPartArray[].partId + color + sizeVariant SKUOne row per part
ColorArray[].hexVariant swatchPrefer hex over colorName
medias (from expansion)Product galleryFilter by mediaType=PRIMARY_IMAGE for the hero
combined_ppc (from expansion)Price tiersChoose list / net / customer per your pricing policy
inventory (from expansion)Stock levelSum across FOB points or store per-warehouse
FobPointArrayShip-from originUseful for freight estimates
classifications.google (from expansion)Google Product CategoryFor Google Shopping / Merchant Center feeds

Things that need a translation layer

⚠️

These fields cannot be imported as-is. Every supplier uses its own vocabulary, and every merchant backend has its own. Build a translation layer and keep it under version control.

  • Categories: ProductCategoryArray and mainCategory use the supplier’s taxonomy, which rarely matches yours. Maintain a mapping table (supplier_code, supplier_categoryyour_category_id) and fail-open to a default (e.g. Uncategorized) for unseen values. Review unmapped categories periodically.
  • Decoration locations: LocationDecoration.locationName values (SIDE1, FRONT, LC, LEFT CHEST, …) vary by supplier. Map to your standard location vocabulary.
  • Decoration methods: LocationDecoration.decorationName (Silk Screen vs. Screen Print vs. SP) needs normalization.
  • Colors: prefer Color.hex for visual matching; colorName and approximatePms differ across suppliers, and standardColorName is often null.
  • Sizes: apparel parts expose ApparelSize (with labelSize, apparelStyle, sizeGroup); hard goods do not. Your importer must handle both shapes.
  • Units of measure: Dimension.dimensionUom and weightUom mix IN/CM and LB/KG across suppliers. Convert to a canonical unit on import.

Step 5. Keep the import fresh

A one-shot import is rarely enough. For ongoing sync:

  • Product changes: use How to get products modified since to pull only products that changed since your last run.
  • Closeouts: How to get closeout products lets you flag items for retirement.
  • Inventory-only refresh: hitting the detail endpoint for every SKU is overkill when you only need stock. Use the cached inventory endpoint:
curl -X GET "https://api.psrestful.com/extra/v2/inventory/HIT?last_modified__since=1h&page_size=500" \
  -H "X-API-Key: your-api-key"

See Extra APIs: Inventory for all filters.


Putting it together

A minimal, backend-agnostic importer. The upsert_product(...) function is where your backend-specific code lives: BigCommerce REST calls, an Odoo RPC client, SQLAlchemy models, a CSV writer, whatever you need.

import requests
from psdomain.model.product_data.v_2_0_0 import ProductResponseV200
 
API_ROOT = "https://api.psrestful.com"
HEADERS = {"X-API-Key": "your-api-key"}
EXPAND = [("expand", "medias"), ("expand", "combined_ppc"),
          ("expand", "inventory"), ("expand", "classifications")]
 
 
def iter_extra_ids(supplier_code: str):
    params = {"status": "active", "supplier_code": supplier_code,
              "page_size": 100, "page": 1}
    while True:
        page = requests.get(f"{API_ROOT}/extra/v2/products",
                            headers=HEADERS, params=params).json()
        for row in page["results"]:
            yield row["extraId"]
        if page.get("next") is None:
            break
        params["page"] += 1
 
 
def fetch_detail(extra_id: int) -> dict:
    return requests.get(
        f"{API_ROOT}/extra/v2/products/{extra_id}",
        headers=HEADERS,
        params=EXPAND + [("currency", "USD")],
    ).json()
 
 
def upsert_product(detail: dict) -> None:
    """Implement for your backend (BigCommerce, Odoo, Postgres, ...)."""
    resp = ProductResponseV200.model_validate(
        {"Product": detail, "ServiceMessageArray": None}
    )
    product = resp.Product
    pricing = detail.get("combinedPpc")
    inventory = detail.get("inventory")
    media = detail.get("medias")
    # ...map into your backend and write.
 
 
def run(supplier_code: str) -> None:
    for extra_id in iter_extra_ids(supplier_code):
        upsert_product(fetch_detail(extra_id))
 
 
if __name__ == "__main__":
    run("HIT")