Products
Read the product catalog: names, descriptions, translations, prices, images, configurable options and sellable stock. All endpoints need the products:read scope.
GET /products
Lists products, paginated.
| Query | Description |
|---|---|
updated_since | ISO 8601 timestamp with time zone. Only products changed after it; results are then sorted by updated_at (oldest first) — use it for incremental sync. |
include_inactive | true = also archived products and products not marked as sold (they come with active: false). Default: only active products that are sold. |
type | stock (physical items) or service. |
page, limit | Page from 1; limit default 100, max 200. |
lang | Document language: en, et, fi, sv, nb, da, lv, lt, de, pl, fr, es, nl. name, description and option names are returned in that language when translated, else in the company's own text. All translations are always in translations. |
curl "https://zerpy.app/api/v1/products?limit=100&lang=en" \
-H "Authorization: Bearer $ZERPY_API_KEY"{
"data": [
{
"id": "37c8a1e2-…",
"sku": "SAUNA-1",
"name": "Barrel sauna",
"description": "Thermowood barrel sauna, 4 persons",
"translations": {
"en": { "name": "Barrel sauna", "description": "Thermowood barrel sauna, 4 persons" },
"fi": { "name": "Tynnyrisauna", "description": null }
},
"type": "stock",
"unit": "pcs",
"barcode": null,
"weight_kg": "420",
"category": "Saunas",
"images": [
{
"id": "b51f…",
"url": "https://…/product-images/…/a1b2.webp",
"thumb_url": "https://…/product-images/…/a1b2-thumb.webp",
"width": 1600,
"height": 1200
}
],
"price": { "net": "8650.00", "vat_rate": "24", "gross": "10726.00", "currency": "EUR" },
"options": [
{
"id": "16a6…",
"name": "Heater",
"translations": { "fi": { "name": "Kiuas" } },
"required": true,
"values": [
{
"id": "c105…",
"name": "Harvia Legend",
"translations": {},
"price_delta": { "net": "0.00", "vat_rate": "24", "gross": "0.00", "currency": "EUR" }
},
{
"id": "7d2e…",
"name": "HUUM Hive",
"translations": {},
"price_delta": { "net": "650.00", "vat_rate": "24", "gross": "806.00", "currency": "EUR" }
}
]
}
],
"stock": { "available": "2", "on_hand": "3" },
"active": true,
"updated_at": "2026-11-07T10:00:00.000Z"
}
],
"page": 1, "limit": 100, "total": 1, "has_more": false
}GET /products/{sku}
One product by SKU, in the same shape, wrapped in { "data": { … } }. Accepts ?lang= and ?include_inactive=true like the list. Unknown SKU → 404 not_found.
curl "https://zerpy.app/api/v1/products/SAUNA-1?lang=fi" \
-H "Authorization: Bearer $ZERPY_API_KEY"URL-encode SKUs that contain special characters. Same items as the list: an archived or not-sold product returns 404 unless you add ?include_inactive=true (then it comes with active: false; partner keys never see not-sold products).
Product object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | ZERPY id |
sku | string | Product code — the key you use in orders |
name | string | In ?lang= when translated, else the company's text |
description | string | null | Same language rule as name |
translations | object | All translations: { "en": { "name", "description" } }. Empty object = not translated; a null field = that text falls back to the main text |
type | stock | service | Services have no stock |
unit | string | e.g. pcs, m, kg, h |
barcode | string | null | |
weight_kg | string | null | Decimal string |
category | string | null | Category name |
images | array | See Images |
price | price object | Standard sale price: { net, vat_rate, gross, currency } in the company currency. With a partner key: the partner's own price, null = price on request |
options | array | Option groups of a configurable product, empty for normal products. See Options |
stock | object | null | { available, on_hand } — see Stock. null for services |
active | boolean | false = archived or not sold |
updated_at | string | Last change of the product data |
Partner keys get extra fields (retail_price, quantity_breaks, stock.consignment) — see Partner product feed.
What changes updated_at
- Changes to the product itself (name, price, description, translations, category, …) and adding, removing or reordering images change
updated_atand send theproduct.updatedwebhook. - Renaming or deleting a category touches its products'
updated_atas well. - Stock changes do not change
updated_at— useGET /stockor thestock.changedwebhook. - Option changes (groups, values, prices, option translations) do not change
updated_at. Re-read options in a periodic full sync.
Images
"images": [
{ "id": "b51f…", "url": "https://…/a1b2.webp", "thumb_url": "https://…/a1b2-thumb.webp", "width": 1600, "height": 1200 }
]- The first image is the main image.
- WebP files:
urlis at most 1600 px,thumb_urlat most 400 px.width/heightare the size of the large image. - URLs are public and stable — safe to hotlink or to copy into your shop.
Translations and ?lang=
Product names and descriptions are written in the company's own language; other languages are added as translations in ZERPY.
- Without
?lang=(company key):name/descriptionare the company's own texts. - With
?lang=fi: the Finnish translation when it exists, otherwise the company's text. translationsalways contains every language, so a multi-language shop can read everything in one call — or call once per language with?lang=.- Option groups and values have
translationstoo:{ "fi": { "name": "Kiuas" } }.
Configurable products (options)
A configurable product (for example a sauna with a choice of heater and facade) lists its option groups in options:
| Field | Description |
|---|---|
options[].id | Option group id |
options[].name | Group name (language rule as above) |
options[].required | true = a value must be chosen when ordering |
options[].values[].id | Option value id — send it in POST /orders |
options[].values[].name | Value name |
options[].values[].price_delta | Price object added to the product price when this value is chosen (with a partner key: in the partner's prices) |
Choose one value per group. Your shop shows the options and computes the final price itself; the order line's unit_price must already include the option prices. See Orders → options.
GET /feed.csv
The whole catalog as one CSV file — for shop import plugins, Google Sheets (=IMPORTDATA(url)) and Excel.
| Query | Description |
|---|---|
key | The API key (alternative to the Authorization header — this is the only endpoint that accepts it in the URL) |
include_inactive | true = also archived products |
lang | Names and descriptions in this language when translated |
curl -o products.csv "https://zerpy.app/api/v1/feed.csv?key=$ZERPY_API_KEY&lang=en"- Products that are sold; ordered by SKU; at most 10 000 rows.
- UTF-8 with BOM, comma-separated, header row, CRLF line ends.
- Text that a spreadsheet would treat as a formula (starting with
=,+,-,@) is prefixed with'. - Columns, in this order:
sku, name, description, category, type, unit, barcode, weight_kg, price_net, vat_rate, price_gross, retail_price_net, retail_price_gross, currency, available, consignment, image_url, image_urls, active, updated_at, retail_currency
| Column | Notes |
|---|---|
price_net, price_gross, vat_rate, currency | The price (with a partner key: the partner's price). Empty price = price on request |
retail_price_net, retail_price_gross, retail_currency | Partner keys only: your standard price |
available | Sellable stock; empty for services |
consignment | Partner keys only: your stock at the partner |
image_url | Main image; image_urls = all images separated by | |
active | true / false |
WARNING
A URL with ?key= works like a password. It ends up in the logs of whoever fetches it — do not publish it. If it leaks, revoke the key and create a new one.