Products & Prices

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

ParameterTypeRequiredDefaultNotes
accountintegeryes
detailstringnosummarysummary or full. Anything else is a 400, INVALID_CHOICE. See Bulk Export (Seeding)
limitintegerno100 at summary, 25 at fullMaximum 100 at summary, 25 at full. See Making Requests, "Page size limits"
cursorstringno

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

CodeWhen
400account missing, limit out of range, malformed cursor, detail not summary or full
401Missing, invalid or expired token
403The account is not a retailer account
404The collection does not exist, or is not visible to you
404At 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
405Any verb other than GET
429Out 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

ParameterTypeRequiredNotes
accountintegeryes
collection_idintegerno, but strongly recommendedApplies 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=1146216

Response

{
  "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:

FieldEstablished hereStill to confirm
idinteger
name, product_numberpresent at both depths
code, description, country_of_origin, heel_height, source_of_materialsstrings in the examplenullability, maximum length
contains_fur, made_to_orderbooleanswhether either can be null
order_minimumnullable, null in the example at product level and 0 at color levelunit and meaning of 0 versus null
updated_attimestamp. Do not use it as a change detector; use version_at from a webhook payload instead
categoriesarray of {gender, category, subcategory} triples, each {id, name} and gender also codewhether a product can carry more than one triple, and whether the sets are enumerable
fabrication, silhouette{id, name} objectsnullability
materials, tags, badgesarrays; tags[].detail nullablenullability of the arrays themselves
imagesarray 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" belowthe 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.

WhereWhat it is
prices[] at the top levelThe 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 id in images means a new image.
  • A previously seen id missing from images means 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

CodeWhen
400account missing, collection_id not an integer
401Missing, invalid or expired token
403The account is not a retailer account, or is not yours to act for
404The 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
405Any verb other than GET
429Out of credits