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:
- Discover: call
GET /extra/v2/productswith filters (status=active,supplier_code=...) and walk the pagination. Save each result’sextraId. - Hydrate: for every
extraId, callGET /extra/v2/products/{extra_id}withexpand=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
| Parameter | Type | Description |
|---|---|---|
status | string | draft, active, closeout, discontinued |
supplier_code | string | Import one supplier at a time (e.g. HIT, SanMar) |
brand | integer | Filter to a single brand |
main_category | string | Supplier’s top-level category |
is_rush_service | boolean | Only products with rush service |
lead_time__range | string | e.g. 5,10 for 5 to 10 day lead times |
list_price__range | string | e.g. 0,25 for items priced ≤ $25 |
search | string | Matches name, productId, part SKUs, GTINs |
ordering | string | name, 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"] += 1Step 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¤cy=USD" \
-H "X-API-Key: your-api-key"Expansion options
| Expansion | What you get | Use for |
|---|---|---|
medias | Images, art templates, and videos per product / part | Product gallery, color swatches, decoration previews |
combined_ppc | List / net / customer pricing plus decoration pricing | Price lists, customer-specific pricing |
inventory | Stock quantity per FOB point / warehouse | Availability, future available when supplier supports INV 2.0.0 |
classifications | Google, Shopify categories | SEO, 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.gitKey models relevant to an import flow:
| Payload | Model | Import |
|---|---|---|
| Standard Product Data 2.0.0 response | ProductResponseV200 | from psdomain.model.product_data.v_2_0_0 import ProductResponseV200 |
| Media Content | MediaContent | from psdomain.model.media_content import MediaContent |
| Configuration & Pricing | ConfigurationAndPricingResponse | from psdomain.model.ppc import ConfigurationAndPricingResponse |
| Inventory 2.0.0 | InventoryLevelsResponseV200 | from 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 field | Typical target field | Notes |
|---|---|---|
supplierCode + productId | SKU or external reference | productId is not globally unique; always qualify it with supplierCode |
productName | Title | |
description (list of strings) | Body / long description | Join with \n\n |
productBrand, lineName | Brand / product line | |
primaryImageURL | Hero image | Fall back to the first medias entry |
ProductPartArray[].partId + color + size | Variant SKU | One row per part |
ColorArray[].hex | Variant swatch | Prefer hex over colorName |
medias (from expansion) | Product gallery | Filter by mediaType=PRIMARY_IMAGE for the hero |
combined_ppc (from expansion) | Price tiers | Choose list / net / customer per your pricing policy |
inventory (from expansion) | Stock level | Sum across FOB points or store per-warehouse |
FobPointArray | Ship-from origin | Useful for freight estimates |
classifications.google (from expansion) | Google Product Category | For 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:
ProductCategoryArrayandmainCategoryuse the supplier’s taxonomy, which rarely matches yours. Maintain a mapping table (supplier_code,supplier_category→your_category_id) and fail-open to a default (e.g.Uncategorized) for unseen values. Review unmapped categories periodically. - Decoration locations:
LocationDecoration.locationNamevalues (SIDE1,FRONT,LC,LEFT CHEST, …) vary by supplier. Map to your standard location vocabulary. - Decoration methods:
LocationDecoration.decorationName(Silk Screenvs.Screen Printvs.SP) needs normalization. - Colors: prefer
Color.hexfor visual matching;colorNameandapproximatePmsdiffer across suppliers, andstandardColorNameis oftennull. - Sizes: apparel parts expose
ApparelSize(withlabelSize,apparelStyle,sizeGroup); hard goods do not. Your importer must handle both shapes. - Units of measure:
Dimension.dimensionUomandweightUommixIN/CMandLB/KGacross 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")