Summary and full depth, the two price levels, and image URL semantics.
Two depths of the same resource: a summary shape for grids and listings, and a full shape with colors, SKUs and prices. Every request requires account. See Making Requests for the response envelope and pagination rules shared with every other list endpoint.
Products, summary depth
GET /v4/retailers/collections/{collection_id}/products
The products in a collection, at the depth you need for a grid. No prices at this depth: prices require full detail.
Parameters
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
account | integer | yes | ||
detail | string | no | summary | summary or full. Anything else is a 400, INVALID_CHOICE. See Bulk Export (Seeding) |
limit | integer | no | 100 at summary, 25 at full | Maximum 100 at summary, 25 at full. See Making Requests, "Page size limits" |
cursor | string | no |
Products come back in the order the brand arranged them in the collection, which is what you want for a grid.
Response
{
"data": [
{
"id": 12345,
"name": "Versilia 105",
"product_number": "E000264MEDOKID",
"categories": [
{
"gender": { "id": 0, "code": "womens", "name": "Womens" },
"category": { "id": 14, "name": "Shoes" },
"subcategory": { "id": 87, "name": "Pumps" }
}
],
"primary_image": { "id": 88123, "url": "https://cdn.jooraccess.com/..." },
"colors": [
{ "id": 456, "code": "BLK", "name": "Black", "swatch": { "id": 9901, "url": "https://cdn.jooraccess.com/..." } }
]
}
],
"errors": [],
"pagination": { "limit": 100, "next_cursor": "WzQyLDEyMzQ1XQ==", "next_page": "/v4/retailers/collections/1146216/products?account=40975&limit=100&cursor=WzQyLDEyMzQ1XQ==" }
}Every product at this depth has exactly these six fields: id, name, product_number, categories, primary_image, colors. primary_image can be null.
Note that colors at summary depth carries a swatch but no images and no skus. That is the difference from full depth, not an empty result.
Errors
| Code | When |
|---|---|
400 | account missing, limit out of range, malformed cursor, detail not summary or full |
401 | Missing, invalid or expired token |
403 | The account is not a retailer account |
404 | The collection does not exist, or is not visible to you |
404 | At detail=full only: a product on the page stopped being visible between the listing step and the detail fetch. status: "NOT_FOUND", message "A product in this page is no longer visible". Transient, and not a statement about the collection: re request the same page, and if it keeps failing move on to the next cursor rather than stalling the seed |
405 | Any verb other than GET |
429 | Out of credits |
Product, full detail
GET /v4/retailers/products/{product_id}?collection_id={collection_id}
Everything about one product: colors, SKUs, prices, images, categories, materials, tags, badges.
Parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
account | integer | yes | |
collection_id | integer | no, but strongly recommended | Applies that collection's color exclusions and SKU delivery windows |
Pass collection_id whenever you have it. A brand can exclude specific colors from a specific collection and set per collection delivery windows. Without collection_id you get the product as the brand defines it globally, which may include colors that collection does not actually offer you.
Request
curl -G https://api.jooraccess.com/v4/retailers/products/12345 \
-H "Authorization: Bearer $TOKEN" \
-d account=40975 \
-d collection_id=1146216Response
{
"data": {
"id": 12345,
"name": "Versilia 105",
"product_number": "E000264MEDOKID",
"code": "GR-VER105",
"description": "Suede pump with a 105mm heel.",
"country_of_origin": "Italy",
"heel_height": "10.5 cm",
"contains_fur": false,
"made_to_order": false,
"order_minimum": null,
"source_of_materials": "Italian tanneries",
"updated_at": "2026-08-08T12:33:10Z",
"categories": [
{
"gender": { "id": 0, "code": "womens", "name": "Womens" },
"category": { "id": 14, "name": "Shoes" },
"subcategory": { "id": 87, "name": "Pumps" }
}
],
"fabrication": { "id": 12, "name": "Suede" },
"materials": [ { "id": 5, "name": "Leather" } ],
"silhouette": { "id": 3, "name": "Pump" },
"images": [ { "id": 88123, "url": "https://cdn.jooraccess.com/..." } ],
"tags": [ { "group": { "id": 2, "name": "Fit" }, "value": { "id": 9, "name": "True to size" }, "detail": null } ],
"badges": [ { "id": 4, "name": "Sustainable", "image_url": "https://cdn.jooraccess.com/..." } ],
"colors": [
{
"id": 456,
"code": "BLK",
"name": "Black",
"order_minimum": 0,
"swatch": { "id": 9901, "url": "https://cdn.jooraccess.com/..." },
"images": [ { "id": 9902, "url": "https://cdn.jooraccess.com/..." } ],
"skus": [
{
"size_id": 789,
"size_name": "36",
"upc": "8033963132116",
"delivery_start": "2026-11-01",
"delivery_end": "2026-12-15",
"prices": [
{
"price_type_id": 5,
"price_type_name": "EUR Wholesale",
"wholesale_currency": "EUR",
"wholesale_price": 340.00,
"retail_currency": "EUR",
"suggested_retail_price": 790.00
}
]
}
]
}
],
"prices": [
{
"price_type_id": 5,
"price_type_name": "EUR Wholesale",
"wholesale_currency": "EUR",
"wholesale_price": 340.00,
"retail_currency": "EUR",
"suggested_retail_price": 790.00
}
]
},
"errors": []
}Field reference
The example above is the complete documented shape. This page does not yet carry a per field type and nullability table for it, which is the one thing you need to define your PIM schema. What the page does establish:
| Field | Established here | Still to confirm |
|---|---|---|
id | integer | |
name, product_number | present at both depths | |
code, description, country_of_origin, heel_height, source_of_materials | strings in the example | nullability, maximum length |
contains_fur, made_to_order | booleans | whether either can be null |
order_minimum | nullable, null in the example at product level and 0 at color level | unit and meaning of 0 versus null |
updated_at | timestamp. Do not use it as a change detector; use version_at from a webhook payload instead | |
categories | array of {gender, category, subcategory} triples, each {id, name} and gender also code | whether a product can carry more than one triple, and whether the sets are enumerable |
fabrication, silhouette | {id, name} objects | nullability |
materials, tags, badges | arrays; tags[].detail nullable | nullability of the arrays themselves |
images | array of {id, url}. Ids change on edit, see "Image URLs" below | |
colors[] | {id, code, name, order_minimum, swatch, images, skus} | |
colors[].skus[] | {size_id, size_name, upc, delivery_start, delivery_end, prices} | nullability of upc and the delivery dates, and the sort order of sizes |
prices[] and colors[].skus[].prices[] | {price_type_id, price_type_name, wholesale_currency, wholesale_price, retail_currency, suggested_retail_price}. The money fields are JSON numbers carrying IEEE 754 doubles, see "Understanding prices" below | the standard the currency codes follow |
The gaps in the right hand column are not resolved on this page; ask your JOOR contact for each one before you build a schema around it. Money is settled: it is a double on the wire, and you should still hold it as an exact decimal on your side.
Understanding prices
Prices appear at two levels, and the difference matters.
| Where | What it is |
|---|---|
prices[] at the top level | The product's own prices, exactly as the brand set them |
colors[].skus[].prices[] | The effective price for that specific size, per price type |
The effective price is a merge: a SKU level price wins, and the product level price fills the gaps. So a product priced only at product level still serves a price on every SKU, and comparing the two arrays tells you whether a given price is inherited or specifically set for that size.
Both levels are filtered to the price types attached to your account. You never see a price type that is not yours.
A product with no active sizes still has prices. colors[].skus[] is built from active sizes only, so a made to order or sample product can legitimately return no SKUs while still carrying its own prices[]. Read the top level array, do not conclude the product is unpriced.
Money is a double. wholesale_price and suggested_retail_price are IEEE 754 double precision floats end to end, serialised as JSON numbers. That is the contract rather than an artefact of the example, so 340.00 is a number and not a string. Parse them into an exact decimal type on receipt and do your arithmetic there. A double cannot represent every two decimal money value exactly, so totalling them across a linesheet accumulates error, and a value that round trips through a float in your own stack can come back a cent short. The currency for each price sits next to it in wholesale_currency and retail_currency.
Image URLs
Image URLs are public, stable and never expire. Persist them freely. There is no request signing and no expiry.
An image edit never mutates a URL in place. It creates a new image, with a new id and a new url, and the old id disappears from the array. So:
- A new
idinimagesmeans a new image. - A previously seen
idmissing fromimagesmeans that image was removed or replaced. Drop your copy.
You may hotlink our URLs or mirror them on ingest. Mirroring is the more conservative choice, because how long a replaced image stays reachable on our CDN is not part of this contract.
Errors
| Code | When |
|---|---|
400 | account missing, collection_id not an integer |
401 | Missing, invalid or expired token |
403 | The account is not a retailer account, or is not yours to act for |
404 | The product does not exist, or is not visible to you. Remember that visibility is both halves of the rule: shared collection and a price type you hold |
405 | Any verb other than GET |
429 | Out of credits |
