Catalog API
Create, update, and manage your product catalog. Sync stock levels and run batch operations for large catalogs. All field names are camelCase; unknown fields are silently ignored.
Prices are not managed via Connect. A product has no single price on Scoperly — pricing is per buyer, resolved through price lists managed in the supplier portal. Sending unitPrice or currency in any catalog payload fails with 422 PRICE_NOT_SUPPORTED_VIA_CONNECT. Remove both fields from your integration.
Products
/connect/v1/supplier/catalog/productsscope: catalog:readList all products in your catalog with cursor-based pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
cursor | integer | Cursor from previous response (last product ID). Omit for first page. |
limit | integer | Results per page (1 — 100, default 50). |
For richer filtering (search by text, status, stock availability), use the /products/search endpoint below.
curl "https://api.scoperly.com/connect/v1/supplier/catalog/products" \
-H "Authorization: Bearer $TOKEN"
# Response:
# {
# "items": [
# {
# "externalId": "SKU-001",
# "name": "Organic Green Tea 500g",
# "categoryName": "Food, Beverages & Tobacco",
# "unitOfMeasure": "pcs",
# "minOrderQty": 5,
# "availableQty": 500,
# "brandName": "Georgian Highlands",
# "barcode": "4601234567890",
# "description": "Premium loose leaf green tea.",
# "images": "[\"https://storage.googleapis.com/.../products/1/42/ab12...cd.jpg\"]",
# "status": "ACTIVE",
# "createdAt": "2025-01-15T10:30:00Z"
# }
# ],
# "nextCursor": "42",
# "hasMore": true
# }
# Note: "images" is a JSON-encoded string containing an array of image URLs —
# parse it before use. No prices in catalog responses (see the pricing note above)./connect/v1/supplier/catalog/productsscope: catalog:writeCreate or update a product by externalId (upsert). If a product with the same externalId already exists for your tenant, it is updated. Supports Idempotency-Key.
Request Body
| Parameter | Type | Description |
|---|---|---|
externalIdrequired | string | Your unique product identifier (e.g. ERP SKU). Max 255 chars. Used for upsert matching. |
namerequired | string | Product name. Max 500 chars. |
description | string | Product description. Max 8 000 chars. |
categorySlug | string | Accepted but ignored in v1 — the category is managed in the supplier portal and is never changed by a Connect upsert. Reserved for a future version. Max 200 chars. |
unitOfMeasure | string | e.g. "pcs", "kg", "liter", "box". Max 50 chars. |
availableQty | decimal | Available stock quantity (0 — 9 999 999.999). On update, omitting it leaves the current value unchanged. Unlimited stock is a portal-managed state and cannot be set via Connect. |
minOrderQty | decimal | Minimum order quantity (default 1). |
stepQty | decimal | Order quantity must be a multiple of this value (default 1). |
leadTimeDays | integer | Days between order acceptance and dispatch (0 — 3650). |
brand | string | Brand / manufacturer name. Max 255 chars. |
barcode | string | Product barcode (EAN-13, UPC, etc.). Max 100 chars. |
attributes | object | Free-form key/value pairs (max 100 entries). E.g. { "fatPercent": 3.2, "shelfLifeDays": 7 }. |
isActive | boolean | Default true. Set false to hide from marketplace without deleting. |
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/products \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"externalId": "SKU-001",
"name": "Organic Green Tea 500g",
"description": "Premium loose leaf green tea from Georgian highlands.",
"categorySlug": "food-dairy",
"unitOfMeasure": "pcs",
"availableQty": 500,
"minOrderQty": 5,
"leadTimeDays": 1,
"brand": "Georgian Highlands",
"barcode": "4601234567890",
"attributes": { "shelfLifeDays": 365, "origin": "GE" },
"isActive": true
}'
# Response: 200 OK
# (same shape as list item, sanitized — no internal tenant / storefront / category IDs)/connect/v1/supplier/catalog/products/:externalIdscope: catalog:writeSoft-delete / unpublish a product by externalId. Supports Idempotency-Key. Returns 204 No Content. Repeating the delete in a new request returns 404 (treat as already deleted); replaying the same Idempotency-Key returns the cached 204.
Field Constraints
| Field | Constraints |
|---|---|
externalId | String, NotBlank, max 255 chars, unique per supplier, immutable after creation. |
name | String, NotBlank, max 500 chars. |
unitOfMeasure | String, max 50 chars. Common: "pcs", "kg", "liter", "box". |
availableQty | Decimal 0 — 9 999 999.999. Omit on update to leave the current value unchanged. Unlimited stock is managed in the supplier portal, not via Connect. |
description | String, max 8 000 chars. |
attributes | Map, max 100 entries. Body size capped at 2 MB for the whole request. |
Silent-drop warning: the server accepts requests with unknown fields (e.g. sending min_order_qty instead of minOrderQty) and silently ignores them. Your product will be created without that field populated. Always use camelCase names exactly as documented. The one exception is price: unitPrice / currency are explicitly rejected with 422 PRICE_NOT_SUPPORTED_VIA_CONNECT rather than ignored — prices are managed through price lists in the supplier portal.
Pagination
Cursors are opaque strings returned by the API under nextCursor. Always pass the cursor verbatim into the next request's cursor query parameter. Do not construct or modify cursors. If a cursor becomes invalid, restart pagination without it.
Product Search
/connect/v1/supplier/catalog/products/searchscope: catalog:readCase-insensitive substring search on product name with optional status filter.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
q | string | Search query — case-insensitive substring match on product name only (no minimum length). |
status | string | Filter by status: DRAFT, ACTIVE, PAUSED, DISCONTINUED. |
cursor | integer | Pagination cursor (last product ID). |
limit | integer | Results per page (1 — 100, default 20). |
curl "https://api.scoperly.com/connect/v1/supplier/catalog/products/search?q=green+tea&status=ACTIVE&limit=10" \
-H "Authorization: Bearer $TOKEN"Stock Management
/connect/v1/supplier/catalog/products/:externalId/stockscope: catalog:writeUpdate stock quantity for a product by externalId. Supports Idempotency-Key.
Request Body
| Parameter | Type | Description |
|---|---|---|
availableQtyrequired | decimal | New available quantity (0 — 9 999 999.999). Required — omitting it returns 400. Unlimited stock is a portal-managed state and cannot be set via Connect. |
curl -X PUT https://api.scoperly.com/connect/v1/supplier/catalog/products/SKU-001/stock \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "availableQty": 250 }'
# Response: 200 OK
# (sanitized ProductResponse with the updated availableQty)Batch Operations
For large catalogs, use batch endpoints. Batch upsert accepts up to 100 products per call; batch stock update accepts up to 50 items. Partial failures are reported per-item with a structured errorCode.
/connect/v1/supplier/catalog/products/batchscope: catalog:writeCreate or update up to 100 products in one request. Each item follows the single-upsert schema. Supports Idempotency-Key.
Body: array of product objects (not wrapped in a container):
[
{ "externalId": "SKU-001", "name": "Green Tea 500g", "availableQty": 500 },
{ "externalId": "SKU-002", "name": "Black Tea 500g", "availableQty": 120 }
]Response shape:
{
"succeeded": 2,
"failed": 0,
"results": [
{ "externalId": "SKU-001", "status": "ACTIVE" },
{ "externalId": "SKU-002", "status": "ACTIVE" }
],
"errors": []
}/connect/v1/supplier/catalog/products/batch/stockscope: catalog:writeUpdate stock for up to 50 products in one call. Supports Idempotency-Key.
Body: wrap items in an items array:
{
"items": [
{ "externalId": "SKU-001", "availableQty": 250 },
{ "externalId": "SKU-002", "availableQty": 100 }
]
}Response shape — succeeded is the count of items applied; each failed item appears in errors:
{
"succeeded": 1,
"errors": [
{
"externalId": "SKU-002",
"status": "failed",
"errorCode": "NOT_FOUND",
"error": "Product not found: SKU-002"
}
]
}Product Images
Publishing a product image is a three-step flow: presign (reserve an upload slot), upload (PUT the bytes straight to storage — they never pass through the API), then commit (make the uploaded image the product's picture). Supported types: image/jpeg, image/png, image/webp; max 5 MB. Thumbnails are generated automatically after commit.
/connect/v1/supplier/catalog/products/:externalId/images/presignscope: catalog:writeReserve an upload slot for an image. Returns a presigned upload URL valid for 15 minutes. No Idempotency-Key — each retry simply gets a fresh URL.
Request Body
| Parameter | Type | Description |
|---|---|---|
contentTyperequired | string | One of image/jpeg, image/png, image/webp. |
sha256required | string | SHA-256 digest of the bytes you are about to upload — 64 lowercase hex characters. Echoed back at commit to prove you are committing the same image you uploaded. |
byteSizerequired | integer | Size of the bytes in bytes (1 — 5 242 880). Oversize is rejected here instead of failing the upload. |
Response shape:
{
"mediaId": "products/.../<sha256>-<uuid>.jpg",
"uploadUrl": "https://storage.googleapis.com/...",
"expiresAt": "2025-01-15T10:45:00Z"
}Then PUT the raw bytes to uploadUrl with Content-Type equal to the presigned contentType and no Authorization header — the URL is self-authenticating, and adding a bearer token makes storage reject the request. Treat mediaId as an opaque handle; do not parse it.
/connect/v1/supplier/catalog/products/:externalId/images/commitscope: catalog:writeMake the uploaded image the product's picture. Supports Idempotency-Key. Commit REPLACES the product's image — it does not append to a gallery.
Request Body
| Parameter | Type | Description |
|---|---|---|
mediaIdrequired | string | The opaque handle returned by presign. |
sha256required | string | The same digest sent at presign. A mismatch (or a handle issued for another product / digest) fails with 422 MEDIA_HANDLE_INVALID. |
Response shape:
{
"externalId": "SKU-001",
"images": ["https://storage.googleapis.com/.../products/1/42/ab12...cd.jpg"]
}After commit, the image URL appears in the product's images field on catalog reads, and thumbnail variants are generated automatically in the background.
/connect/v1/supplier/catalog/products/:externalId/imagesscope: catalog:writeRemove the product's picture. Supports Idempotency-Key. Returns the (now empty) images list.
# 1. Presign
SHA=$(sha256sum photo.jpg | cut -d' ' -f1)
SIZE=$(stat -c%s photo.jpg)
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/products/SKU-001/images/presign \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"contentType\": \"image/jpeg\", \"sha256\": \"$SHA\", \"byteSize\": $SIZE }"
# → { "mediaId": "...", "uploadUrl": "https://storage...", "expiresAt": "..." }
# 2. Upload the bytes (NO Authorization header)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg
# 3. Commit
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/products/SKU-001/images/commit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{ \"mediaId\": \"$MEDIA_ID\", \"sha256\": \"$SHA\" }"
# → { "externalId": "SKU-001", "images": ["https://storage..."] }Errors: 422 MEDIA_TYPE_NOT_SUPPORTED (not jpeg/png/webp), 422 MEDIA_TOO_LARGE (over 5 MB), 422 MEDIA_HANDLE_INVALID (digest mismatch or a handle that belongs to another product), 404 PRODUCT_NOT_FOUND (unknown externalId).
Sync Sessions
Sync sessions give observability for large import/sync jobs — progress counts, error summaries, and a historical log. Products are pushed via the normal batch upsert endpoint; the sync session tracks the enclosing run.
/connect/v1/supplier/catalog/sync/startscope: catalog:writeStart a new sync session. Returns the session UUID to correlate follow-up batch upserts.
/connect/v1/supplier/catalog/sync/{sessionId}/endscope: catalog:writeMark a sync session complete. Returns total processed / failed counts.
/connect/v1/supplier/catalog/sync/historyscope: catalog:readList the last 20 sync sessions for this tenant.
# 1. Start session
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/sync/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"integrationType": "SMART_SALE",
"direction": "INBOUND",
"totalItems": 1240
}'
# → 201 Created
# { "sessionId": "5a1e...-...-...-...-...", "status": "RUNNING" }
# 2. Batch upsert products (up to 100 items per call)
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/products/batch \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '[ { "externalId": "SKU-1", "name": "...", "availableQty": 100 }, ... ]'
# 3. End session
curl -X POST https://api.scoperly.com/connect/v1/supplier/catalog/sync/5a1e.../end \
-H "Authorization: Bearer $TOKEN"
# { "sessionId": "5a1e...", "status": "COMPLETED", "processedItems": 1237, "failedItems": 3 }