Skip to content

Catalog

Build and maintain your catalog here: products and their variants, categories and tags, product images, and the EU compliance duties on your listings (GPSR safety identity and the Omnibus prior-price rule).

You drive the catalog through the admin API under /admin/v1/. Every write requires an authenticated user with the right permission (products:write, categories:write, or categories:delete). Each mutation records an audit row with your user id, IP, and user agent.

SovEcom admin — Products list

Store prices as integer minor units plus a currency code. A product priced at €19.99 stores priceAmount: 1999 and currency: "EUR". Never send a decimal. Send the currency as an ISO-4217 three-letter code; the API upper-cases it on the way in, so eur and EUR both land as EUR.

FieldTypeMeaning
priceAmountinteger ≥ 0Selling price in minor units (cents)
currency3-letter codeISO-4217, e.g. EUR, USD
compareAtAmountinteger ≥ 0, nullableStrike-through reference price (see Omnibus)

A product is the listing. It carries a title, a URL slug, a description, an SEO title and description, a status, and a metadata JSON blob. Pricing and stock live on its variants, never on the product itself.

POST /admin/v1/products (permission products:write).

{
"title": "Merino Wool Beanie",
"description": "Soft, warm, ethically sourced.",
"status": "draft",
"variants": [
{ "sku": "BEANIE-GREY", "title": "Grey", "priceAmount": 2900, "currency": "EUR", "stockQuantity": 40 }
]
}

What happens on create:

  • Slug. Omit slug and SovEcom derives one from the title (merino-wool-beanie). A collision appends -2, -3, and so on. A title with no Latin characters falls back to a short unique suffix.
  • Default variant. Send no variants array and SovEcom creates one default variant at priceAmount: 0 in your store default currency (STORE_DEFAULT_CURRENCY, defaults to EUR). Edit it before you publish.
  • Status. New products default to draft. Valid values are draft, published, archived.

SovEcom admin — New product form

You cannot publish a product whose variant prices are still zero. status: "published" fails with a 422 if any variant has priceAmount: 0 and carries no free flag. To ship a free item, set that variant’s options.free to true:

{ "title": "Free sample", "priceAmount": 0, "currency": "EUR", "options": { "free": true } }

Set every priced variant to at least 1 cent, or flag it free. The guard runs both when you create a product as published and when you PATCH an existing draft to published.

PATCH /admin/v1/products/:id (permission products:write). PATCH semantics: send only the fields you change. You can update title, slug, description, status, seoTitle, seoDescription, and isBundle. Changing the slug regenerates a unique one if your value collides.

DELETE /admin/v1/products/:id (permission products:write). This is a hard delete. The cascade removes the product’s variants and image links.

GET /admin/v1/products (permission products:read) returns an offset-paginated list. Query parameters:

ParamValuesDefault
page≥ 11
pageSize1–20020
statusdraft / published / archivedall
categorycategory UUID
tagtag UUID
priceMin / priceMaxinteger cents
inStocktrue / false
sortcreated / title / pricecreated
orderasc / descdesc

A variant is the buyable unit: one SKU, one price, one stock count. Every product has at least one. Use variants for size, colour, or any option that changes price or inventory.

POST /admin/v1/products/:productId/variants adds a variant. PATCH .../variants/:variantId updates one. DELETE .../variants/:variantId removes one. POST .../variants/reorder sets display order. All four need products:write.

FieldTypeNotes
skustringUnique per store. A collision gets a short suffix appended.
titlestring, nullableVariant label, e.g. “Large / Blue”
optionsJSON objectFree-form attributes; options.free = true exempts a 0-price variant from the publish guard
priceAmountinteger centsRequired on create
currency3-letter codeMust match the product’s other variants
compareAtAmountinteger cents, nullableStrike-through price; read the Omnibus rule below before you set it
stockQuantityinteger ≥ 0On-hand count
allowBackorderbooleanSell past zero stock when true
weightGrams, lengthMm, widthMm, heightMminteger, nullableUsed by shipping rate rules. See Shipping.

On a variant PATCH, send currency whenever you send priceAmount. Send the pair together or the request fails.

Categories are a hierarchy. Each category has a name, a slug, an optional parentId, a display position, and its own SEO fields. Build a tree by pointing children at a parent.

POST /admin/v1/categories (permission categories:write) creates one. PATCH /admin/v1/categories/:id renames, re-slugs, re-parents, or repositions. GET /admin/v1/categories returns a flat list with each node’s parentId so you can rebuild the tree client-side.

DELETE /admin/v1/categories/:id needs categories:delete. Deletion is blocked while the category has children: the API returns 409. Move or delete the child categories first, then delete the parent.

Assign categories to a product with PUT /admin/v1/products/:id/categories (permission products:write). This is replace-set: the body’s categoryIds array becomes the product’s complete category set. Send the full list every time, not a delta.

{ "categoryIds": ["018f...a1", "018f...b2"] }

Tags are flat labels with a name and a slug. No hierarchy, no SEO fields. POST /admin/v1/tags (permission categories:write) creates one; tag write and read reuse the category permissions. DELETE /admin/v1/tags/:id (permission categories:delete) removes a tag and unlinks it from every product.

Assign tags to a product with PUT /admin/v1/products/:id/tags, replace-set, same shape as categories with a tagIds array.

Upload first, then attach. Images are tenant-scoped assets you link to one or more products.

POST /admin/v1/images (permission products:write), multipart/form-data with a file field and an optional alt_text query parameter. The file size limit is 10 MB.

On upload SovEcom:

  • Strips all EXIF, GPS, XMP, and IPTC metadata from every output, including the re-encoded original. Customer-facing images never leak a camera’s GPS coordinates.
  • Bakes in EXIF orientation so rotated phone photos display upright.
  • Generates four sizes, each in AVIF, WebP, and JPEG:
SizeWidth
large1920 px
medium800 px
small400 px
thumbnail150 px

Each output fits inside its target width and never enlarges a smaller source. The response returns public URLs for every size and format, plus the original.

Open a product and choose Edit to manage its details, variants, and images — set alt text per image (required for accessibility and shown when an image fails to load).

SovEcom admin — edit product

  • POST /admin/v1/products/:id/images with { "imageId": "...", "position": 0 } links an uploaded image. Re-attaching the same image returns 409.
  • POST /admin/v1/products/:id/images/reorder with an ordered array of image ids sets display order. Every id must already be attached to that product.
  • DELETE /admin/v1/products/:id/images/:imageId unlinks an image from a product. Deleting the image asset itself is DELETE /admin/v1/images/:id, which removes all generated sizes from storage.

Two EU obligations land on products and pricing: GPSR safety identity and the Omnibus prior-price rule. Read both before you publish to EU customers.

The General Product Safety Regulation (EU 2023/988, in force since 13 December 2024) requires each consumer-product listing to show the manufacturer’s identity and contact, the EU responsible person where the manufacturer sits outside the EU, and any safety or warning information.

The Omnibus Directive (EU 2019/2161) governs how you present a price reduction. Any “was X, now Y” claim must reference the lowest price you applied in the 30 days before the reduction. The original full price and any inflated anchor are both illegal here.

In SovEcom the compareAtAmount field on a variant holds the strike-through reference price. SovEcom stores and displays the figure exactly as you enter it. It keeps no 30-day price history and computes no prior-lowest figure for you.

When you cut a price, update priceAmount to the new selling price and set compareAtAmount to the prior 30-day low. When the promotion ends, clear compareAtAmount (set it to null) so the storefront stops showing a strike-through.

For VAT, the OSS €10,000 threshold, and the 14-day withdrawal right, see Tax and Returns.

ActionMethod + pathPermission
Create productPOST /admin/v1/productsproducts:write
List productsGET /admin/v1/productsproducts:read
Update productPATCH /admin/v1/products/:idproducts:write
Delete productDELETE /admin/v1/products/:idproducts:write
Add variantPOST /admin/v1/products/:id/variantsproducts:write
Update variantPATCH .../variants/:variantIdproducts:write
Upload imagePOST /admin/v1/imagesproducts:write
Attach imagePOST /admin/v1/products/:id/imagesproducts:write
Create categoryPOST /admin/v1/categoriescategories:write
Delete categoryDELETE /admin/v1/categories/:idcategories:delete
Assign categoriesPUT /admin/v1/products/:id/categoriesproducts:write
Create tagPOST /admin/v1/tagscategories:write
Assign tagsPUT /admin/v1/products/:id/tagsproducts:write