Skip to content

Products

Push your product catalog to Humind so the AI assistant can recommend the right items, ground its answers in your data, and surface accurate prices and availability.

The endpoints here cover create, read, update, replace, archive, and batch operations on products, including their variants and embedded translations.

The Product object

FieldTypeRequiredDescription
external_idstringYes (on create)Your unique identifier for this product. Free-form, must be unique per company. Used as the upsert key, posting again with the same value updates the existing product.
humind_idstringReturned only24-char hex ObjectId Humind assigns. Stable for the lifetime of the product.
titlestringYesDisplay title in default_language.
descriptionstringNoPlain-text description.
description_htmlstringNoHTML description. Sanitized server-side, see HTML content.
handlestringNoURL-safe slug, lowercase, hyphen-separated. Optional: when omitted the server derives one from title (lowercase ASCII, non-alphanumerics replaced by hyphens). Not required to be unique within a store; lookup keys are external_id / humind_id, not handle.
type'product' | 'kit'NoDefaults to product. Use kit for items that bundle several SKUs sold as one (rare). Any other value is rejected with a 400 validation_failed error on type.
status'active' | 'archived' | 'draft' | 'unlisted'NoDefaults to active. archived hides a retired product, draft is for staging unpublished products, and unlisted keeps a product commercially available through its direct URL while excluding it from Humind discovery and recommendations.
online_store_urlstringNoPublic URL of the product on your storefront. The assistant uses this to deep-link shoppers from chat.
default_languagestringNoBCP 47 short tag (en, fr, pt-BR). The two-letter primary subtag is required; the optional region subtag must be uppercase. When omitted, the company's primary language is used. Other shapes are rejected with validation_failed.
available_for_salebooleanReturned onlyComputed automatically from variants: true when status is active or unlisted and at least one variant has available_for_sale: true. Any value sent on the product itself is ignored. To make a product unavailable, set every variant's available_for_sale to false, or archive the product (status: archived).
brandobjectNoSee Brand.
categoriesstring[]NoFree-form category names. Used for soft grouping; doesn't have to match Humind collections.
imagesobject[]NoSee Image. The first image is the primary one shown in chat.
metafieldsobject[]NoCustom product metadata. See Metafields. Maximum 250 entries.
group_membershipsobject[]NoProduct families this product belongs to. See Product group memberships. Maximum 20 entries.
variantsobject[]Yes (1 to 250)See Variant. Even single-SKU products need one variant carrying the price and currency. Variant external_ids must be unique within the array; duplicates are rejected with validation_failed.
translationsobjectNoMap of ISO 639-1 codes to per-locale overrides. See Translations.
created_atISO 8601Returned onlyCreation timestamp (UTC).
updated_atISO 8601Returned onlyLast modification timestamp (UTC).

Commercial availability and Humind discovery are separate

An unlisted product can return available_for_sale: true, remain reachable on the merchant's storefront, and still be excluded from Humind indexing. Humind does not generate a search vector or AI questions for it and does not surface it in search or recommendations.

Brand

FieldTypeRequiredDescription
namestringYesBrand display name.
domainstringNoBrand's canonical web domain (no protocol). Used for deduplication and assistant context.

Image

FieldTypeRequiredDescription
urlstringYesPublicly reachable HTTPS URL. Humind fetches and caches the image; ensure the URL is stable.
altstringNoAlt text for accessibility and the assistant's understanding of the image.

Metafields

Use metafields for catalog information that does not belong in the standard product schema, such as travel themes, trip duration, material, care instructions, or an internal ranking.

Each metafield has four required string properties:

FieldConstraintsDescription
namespace1 to 255 characters; letters, numbers, _, ., -Groups related keys. Humind does not impose a business naming convention. Use a stable domain such as travel, erp, or content.
key1 to 255 characters; letters, numbers, _, ., -Stable identifier within the namespace. We recommend snake_case.
type1 to 255 characters; letters, numbers, _, ., -Describes how the value should be interpreted, for example single_line_text_field, list.single_line_text_field, or html.
valueString, maximum 100,000 charactersThe stored value. Lists must be encoded as a JSON string.

The (namespace, key) pair must be unique within each product or variant metafield array. A product or variant accepts at most 250 metafields.

json
{
  "metafields": [
    {
      "namespace": "travel",
      "key": "wish_category_titles",
      "type": "list.single_line_text_field",
      "value": "[\"Art et culture\",\"Voyage en famille\"]"
    },
    {
      "namespace": "travel",
      "key": "marketing_description_key_points",
      "type": "html",
      "value": "<ul><li>Explore the tea fields</li></ul>"
    }
  ]
}

Use type: "html" when the value contains HTML. Humind sanitizes it on input and removes scripts, event handlers, inline styles, and unsafe URLs. Values with any other type are stored as opaque strings.

Metafields are accepted by POST /products, POST /products/batch, PUT /products/{id}, PATCH /products/{id}, PUT /products/{id}/variants/{variant_external_id}, and every product line in an NDJSON import. They are also returned by product read and write responses.

For PATCH /products/{id}, omitting metafields preserves the existing product metafields. Sending the property replaces the complete product metafield array, and sending [] clears it. On the variant sub-resource, the optional metafields property also replaces the complete metafield array of that variant.

Push AI-relevant data at product level, not variant level

Variant metafields are stored and returned, but the AI assistant, search filters, and the dashboard's metafield configuration only read product-level metafields. Anything the assistant should know (composition, usage advice, scent notes, …) must be pushed on the product, even when it also exists on variants.

Activate your metafields for the AI

Pushing a metafield stores it; it does not yet feed the assistant. Each metafield must be activated (AI inclusion, search filter, display) either in the dashboard or programmatically via PUT /settings/metafields, which lets a fully automated integration push data and activate it in the same pipeline.

Metafields can be translated per language: see Translations.

Product group memberships

Use group_memberships to connect the product to a family created through the Product groups API. Create the group before writing products that reference it.

FieldTypeRequiredDescription
group_external_idstringYesexternal_id of an existing public API product group. Unique within the product's membership array.
selected_optionsobject[]NoValues selected for this product. Each option can appear once.
selected_options[].option_external_idstringYesexternal_id of an option defined on the referenced group.
selected_options[].valuestringYesThis product's value for the option, for example Blue or Velvet.
json
{
  "group_memberships": [
    {
      "group_external_id": "sofa-model-1",
      "selected_options": [
        { "option_external_id": "color", "value": "Blue" },
        { "option_external_id": "material", "value": "Velvet" }
      ]
    }
  ]
}

Omitting group_memberships preserves the existing public API memberships. Sending [] clears them. Memberships managed by Shopify or another internal connector remain untouched in both cases.

Unknown references return 422 product_group_not_found or 422 product_group_option_not_found. The same field and semantics apply to synchronous product endpoints and NDJSON imports.

Use related_products and complementary_products to give the assistant merchant-curated product recommendations, mirroring the semantics of Shopify Search & Discovery:

  • related_products: alternatives to this product ("you may also like").
  • complementary_products: products that go well with this one ("pairs well with"), which the assistant proactively suggests as cross-sell when the shopper shows buying intent.

Both fields are arrays of product external_id references, maximum 25 entries each, unique within the array:

json
{
  "external_id": "eau-de-parfum-belle-50ml",
  "related_products": ["eau-de-toilette-belle-100ml"],
  "complementary_products": ["creme-corps-belle-200ml", "savon-belle"]
}

Reference semantics:

  • References point to products of your own catalog by their external_id. Create order does not matter: an id whose product has not been pushed yet is stored as-is and starts resolving as soon as the referenced product exists.
  • References to products that are archived, draft, or otherwise not surfaced by Humind resolve to nothing; the assistant never recommends them.
  • Self-references are silently ignored.
  • Same update semantics as metafields: omitting the property preserves the stored list, sending it replaces the whole list, [] clears it. Accepted by the synchronous product endpoints and every product line of an NDJSON import.

For Shopify merchants, nothing to push: the recommendations configured in the Search & Discovery app (complementary and related products) are imported automatically on every catalog sync.

Variant

FieldTypeRequiredDescription
external_idstringYesYour unique variant identifier (e.g. SKU). Unique within the product.
titlestringNoVariant label (e.g. "50 ml", "Black / Medium"). Optional: for single-SKU products you can omit it and the variant inherits the product title. Required in practice as soon as a product carries more than one variant, otherwise shoppers can't tell them apart.
skustringNoStock-keeping unit. Surface to shoppers when relevant.
pricenumberYesSelling price as a decimal with at most 2 fractional digits (e.g. 29.90). Must be ≥ 0 and ≤ 1,000,000,000. Values with more than 2 explicit fractional digits (e.g. 29.999) are rejected with validation_failed. Floating-point artefacts that round to ≤2 fractional digits (e.g. 0.300000000000000040.3) are accepted and stored as the rounded value, so retrieving the product back returns the cleaned-up price.
compare_at_pricenumberNoStrike-through "before" price for discounts. Same format as price. Must be strictly greater than price if set: compare_at_price <= price returns validation_failed.
currencystringYesISO 4217 code (e.g. EUR, USD, GBP). Validated against the live ISO 4217 list: unknown codes are rejected with validation_failed.
available_for_salebooleanNoDefaults to true.
inventory_quantityintegerNoStock count. Omit to mark as untracked.
metafieldsobject[]NoCustom variant metadata. Same format and limits as product metafields.
regional_pricingobjectNoMap of ISO 3166-1 alpha-2 country codes to {currency, price, compare_at_price?} for region-specific pricing. See example below.
cart_actionobjectNoWhat happens when the assistant offers an "Add to cart" CTA for this variant. Defaults to { "type": "noop" } when omitted. See Cart action.

Cart action

cart_action is a discriminated union: pick the type that matches your storefront.

type: 'redirect': send the shopper to a URL

FieldTypeRequiredDescription
type'redirect'YesSend the shopper to url.
urlstringYesThe URL to send the shopper to (typically online_store_url with a variant query param, but anything reachable works).
target'product_page' | 'add_to_cart'NoWhat opening url does, which decides the CTA label. product_page → the CTA reads "View product" and links straight through, since the add happens on your page. add_to_carturl is a deep link that fills the cart, and the CTA reads "Add to cart". Omitted means add_to_cart: both are valid and indistinguishable from the URL, so set product_page explicitly rather than let a cart CTA land the shopper on a page with nothing added. See Add to cart.

type: 'noop': show the variant without a CTA

FieldTypeRequiredDescription
type'noop'YesSurface the variant in chat without an actionable CTA (useful when checkout is gated, e.g. B2B).

type: 'prestashop': native PrestaShop AJAX add-to-cart

For PrestaShop 1.7+ / 8 stores, push native id_product and id_product_attribute so the widget can POST directly to your /cart endpoint same-origin (no CORS, no new tab, native cart drawer updates inline).

FieldTypeRequiredDescription
type'prestashop'YesNative PrestaShop add-to-cart.
id_productintegerYesPrestaShop's id_product.
id_product_attributeintegerYesPrestaShop's id_product_attribute for the variant. Use 0 for products without combinations.
product_urlstringYesThe product page URL. Used as a fallback redirect target when window.prestashop is unavailable on the visitor's page.

type: 'shopify': native Shopify AJAX add-to-cart

For Shopify stores, the widget posts to the storefront's /cart/add.js endpoint same-origin. Merchants on the Humind Shopify app never need to push this: the app attaches it automatically at sync time. Push it explicitly only for catalogs managed outside the app.

FieldTypeRequiredDescription
type'shopify'YesNative Shopify add-to-cart.
variant_idintegerYesThe numeric (legacy) variant id accepted by /cart/add.js.
product_urlstringNoFallback product page URL. When omitted, the widget falls back to the product's online_store_url.
propertiesobjectNoLine-item properties attached to the cart line (e.g. engraving text, a source tag). String values only.

type: 'woocommerce': native WooCommerce add-to-cart

For WooCommerce stores, the widget submits the standard add-to-cart request to the product page same-origin, then refreshes the theme's cart fragments.

FieldTypeRequiredDescription
type'woocommerce'YesNative WooCommerce add-to-cart.
product_idintegerYesWooCommerce's parent product id.
variation_idintegerYesThe variation's id. Use 0 for products without variations.
product_urlstringYesThe product page URL the add-to-cart request is submitted to.
attributesobjectNoThe attribute_* values variable products need to disambiguate sibling variations (e.g. { "pa_color": "red", "pa_size": "M" }). String values only.

type: 'function': call your storefront's own add-to-cart function

For custom storefronts that expose a JavaScript quick-buy function on window (for example injected via your tag manager), the widget calls it directly so shoppers stay in the conversation. See the Add to cart guide for the function contract.

FieldTypeRequiredDescription
type'function'YesCall a merchant-exposed window function.
namestringYesThe function's name on window (e.g. myQuickBuy). Must be a plain JavaScript identifier (letters, digits, _, $, not starting with a digit, max 64 chars): the widget looks it up as a window property and never evaluates an expression.
argsobjectNoFlat string map passed to the function as its single argument, e.g. { "productId": "SKU123" }. Keys max 64 chars, values max 256 chars.
product_urlstringYesFallback: when the function is not present on the page (visitor outside an AB test bucket, excluded page, tag manager not loaded), the widget redirects the shopper there instead.
cart_urlstringNoYour cart or checkout page. When set, the confirmation toast shown after a successful add offers a "Go to checkout" button that opens it. When omitted, the toast has no button: the widget cannot know where your cart lives.

Auto-derive

If your company is configured with catalogIntegrationType: prestashop, shopify or woocommerce (ask us to flag your account), you can omit cart_action entirely on every variant: the API auto-derives the native action from your external_id (product) and external_id (variant) fields. online_store_url is required on the product for the PrestaShop and WooCommerce derivations. Push cart_action explicitly only when you want to override the derivation for specific variants (for example to add WooCommerce attributes or Shopify properties, which are never auto-derived). An explicit cart_action always wins over the derivation.

These ids are public

PrestaShop, Shopify and WooCommerce product and variant ids are public storefront identifiers: visible in HTML markup, URLs, and data attributes. Pushing them via the API doesn't leak any secret.

Translations

translations is a map of language codes to per-locale field overrides:

json
{
  "default_language": "fr",
  "title": "Crème hydratante",
  "description": "Une crème légère pour le visage.",
  "handle": "creme-hydratante",
  "translations": {
    "en": {
      "title": "Hydrating cream",
      "description": "A lightweight face cream.",
      "handle": "hydrating-cream"
    }
  }
}

Per-locale fields you can override:

FieldType
titlestring
descriptionstring
description_htmlstring
handlestring
online_store_urlstring
ingredientsstring[]
metafieldsobject[]

Keys not present in translations fall back to the top-level (default_language) value.

Translated metafields are per-language overrides matched by (namespace, key): when the assistant serves the product in a given language, an entry in translations.<lang>.metafields replaces the value of the base metafield with the same namespace and key. Base metafields without an override keep their default-language value; a translated entry without a base counterpart is stored but never surfaced. The array follows the same format and limits as product metafields, and only the base list drives metafield activation: no per-language configuration needed.

json
{
  "default_language": "fr",
  "metafields": [
    { "namespace": "olfactif", "key": "notes_tete", "type": "list.single_line_text_field", "value": "[\"Bergamote\",\"Mandarine\"]" }
  ],
  "translations": {
    "en": {
      "metafields": [
        { "namespace": "olfactif", "key": "notes_tete", "type": "list.single_line_text_field", "value": "[\"Bergamot\",\"Mandarin\"]" }
      ]
    }
  }
}

You can patch one locale at a time without resending the full product via the dedicated Translations sub-resource.

HTML content

Several fields accept HTML, most notably description_html, plus content_html on translations. Humind sanitizes HTML server-side before storing it. The sanitizer applies a fixed allowlist:

  • Tags preserved: p, a, br, hr, em, strong, b, i, u, ul, ol, li, h1 to h6, blockquote, pre, code, table, thead, tbody, tr, th, td, img, span, div.
  • Tags stripped: script, iframe, style, object, embed, form, input, and any other tag not in the list above.
  • Attributes stripped: event handlers (onclick, onerror, …), style, and any javascript: URL.
  • Attributes preserved: href on <a> (only https, http, mailto, and valid data: URIs), src, alt, width, height on <img>, plus a limited set of class values.

HTML is sanitized on input

Don't rely on the API to round-trip your raw HTML. Tags like <script> or attributes like onclick are stripped silently, your stored markup may be a strict subset of what you sent. Read back the resource after a write if you need to see what was kept.


Create or upsert

POST /products creates a new product, or updates an existing one if the external_id already exists for your company.

Required scope: catalog:write

Request

bash
curl -X POST https://api.thehumind.com/public/v1/products \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 3f1a8d92-7c4b-4e6f-b2a1-d5e9c8f7a3b4" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "SKU-123",
    "title": "Crème hydratante",
    "handle": "creme-hydratante",
    "description": "Une crème légère pour le visage.",
    "status": "active",
    "default_language": "fr",
    "online_store_url": "https://example.com/products/creme-hydratante",
    "brand": { "name": "Acme Skincare", "domain": "acme-skincare.com" },
    "categories": ["Soin", "Hydratation"],
    "images": [
      { "url": "https://cdn.example.com/creme.jpg", "alt": "Crème hydratante 50ml" }
    ],
    "variants": [
      {
        "external_id": "SKU-123-50ML",
        "title": "50 ml",
        "sku": "SKU-123-50ML",
        "price": 29.90,
        "compare_at_price": 34.90,
        "currency": "EUR",
        "available_for_sale": true,
        "inventory_quantity": 42,
        "regional_pricing": {
          "US": { "currency": "USD", "price": 32.00 }
        },
        "cart_action": {
          "type": "redirect",
          "url": "https://example.com/products/creme-hydratante?variant=50ml"
        }
      }
    ],
    "translations": {
      "en": {
        "title": "Hydrating cream",
        "description": "A lightweight face cream.",
        "handle": "hydrating-cream"
      }
    }
  }'

Response 201 Created (new product) or 200 OK (existing product updated)

json
{
  "external_id": "SKU-123",
  "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f",
  "title": "Crème hydratante",
  "handle": "creme-hydratante",
  "description": "Une crème légère pour le visage.",
  "status": "active",
  "default_language": "fr",
  "online_store_url": "https://example.com/products/creme-hydratante",
  "available_for_sale": true,
  "brand": { "name": "Acme Skincare", "domain": "acme-skincare.com" },
  "categories": ["Soin", "Hydratation"],
  "images": [
    { "url": "https://cdn.example.com/creme.jpg", "alt": "Crème hydratante 50ml" }
  ],
  "variants": [
    {
      "external_id": "SKU-123-50ML",
      "title": "50 ml",
      "sku": "SKU-123-50ML",
      "price": 29.90,
      "compare_at_price": 34.90,
      "currency": "EUR",
      "available_for_sale": true,
      "inventory_quantity": 42,
      "regional_pricing": {
        "US": { "currency": "USD", "price": 32.00 }
      },
      "cart_action": {
        "type": "redirect",
        "url": "https://example.com/products/creme-hydratante?variant=50ml"
      }
    }
  ],
  "translations": {
    "en": {
      "title": "Hydrating cream",
      "description": "A lightweight face cream.",
      "handle": "hydrating-cream"
    }
  },
  "created_at": "2026-04-25T14:30:00Z",
  "updated_at": "2026-04-25T14:30:00Z"
}

200 vs 201

A 201 Created means this is a new product (first time the API saw this external_id). A 200 OK means the product already existed and was updated in place. Both are success.


List products

GET /products returns the products that belong to the company the API key is bound to.

Required scope: catalog:read

Query parameterTypeDescription
cursorstringOpaque cursor returned as next_cursor in the previous page. Omit on the first call.
limitintegerItems per page (1 to 100). Defaults to 50.
handlestringFilter to a single product by exact handle match.
status'active' | 'draft' | 'archived' | 'unlisted' | 'all'Filter by status. When omitted, returns active, draft, and unlisted products. Use archived for soft-deleted products or all to disable the status filter.

Cursor pagination

List endpoints return up to limit items plus a next_cursor you can pass to fetch the following page. When next_cursor is null, you've reached the end. The cursor is opaque: don't parse it; the format may change in a future version.

Request

bash
curl https://api.thehumind.com/public/v1/products \
  -H "Authorization: Bearer hmd_live_..."

To filter by handle:

bash
curl "https://api.thehumind.com/public/v1/products?handle=creme-hydratante" \
  -H "Authorization: Bearer hmd_live_..."

Response 200 OK

json
{
  "data": [
    {
      "external_id": "SKU-123",
      "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f",
      "title": "Crème hydratante",
      "handle": "creme-hydratante",
      "status": "active",
      "default_language": "fr",
      "variants": [
        {
          "external_id": "SKU-123-50ML",
          "title": "50 ml",
          "price": 29.90,
          "currency": "EUR"
        }
      ],
      "created_at": "2026-04-25T14:30:00Z",
      "updated_at": "2026-04-25T14:30:00Z"
    }
  ]
}

Retrieve a product

GET /products/{id} returns a single product. The {id} accepts either form documented in Identifiers.

Required scope: catalog:read

Request, by external_id

bash
curl https://api.thehumind.com/public/v1/products/api:SKU-123 \
  -H "Authorization: Bearer hmd_live_..."

Request, by humind_id

bash
curl https://api.thehumind.com/public/v1/products/65f1ab9c8e7d4a2b1c3d4e5f \
  -H "Authorization: Bearer hmd_live_..."

Response 200 OK

Same shape as the create response.


Replace a product

PUT /products/{id} performs a full replace: the request body becomes the entire product. Fields you don't include are reset to their defaults (or unset where applicable).

Use this when you have a complete representation of the product on your side and want to mirror it exactly. For partial updates, use PATCH instead.

Required scope: catalog:write

Request

bash
curl -X PUT https://api.thehumind.com/public/v1/products/api:SKU-123 \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 7a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "SKU-123",
    "title": "Crème hydratante (édition limitée)",
    "handle": "creme-hydratante",
    "status": "active",
    "default_language": "fr",
    "variants": [
      { "external_id": "SKU-123-50ML", "title": "50 ml", "price": 32.00, "currency": "EUR" }
    ]
  }'

Response 200 OK

Returns the full product object, same shape as the create response.

Replace is destructive

PUT removes any field, variant, image, or translation not present in the body. If you want to bump a single variant's price, use PATCH instead.


Update a product

PATCH /products/{id} performs a partial update: only the fields present in the body are modified. Everything else stays as it was.

Required scope: catalog:write

Request, bump a single variant's price

bash
curl -X PATCH https://api.thehumind.com/public/v1/products/api:SKU-123 \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d" \
  -H "Content-Type: application/json" \
  -d '{
    "variants": [
      { "external_id": "SKU-123-50ML", "price": 32.00 }
    ]
  }'

When you send a variants array in PATCH, each entry is matched on external_id and merged with the existing variant of the same external ID. Variants you don't list are left untouched. To remove a variant, use PUT (full replace) without that variant in the array.

Response 200 OK

Returns the full updated product object.


Archive (or delete) a product

DELETE /products/{id} performs a soft delete by default: the product's status is set to archived, available_for_sale flips to false, and the assistant stops surfacing the product. The record itself stays so you can restore it later by PATCH-ing the status back to active.

Pass ?force=true to hard delete the product instead: the document is removed from the database after the archive cascade runs. There is no un-delete; only use this when you really want the SKU gone (e.g. test catalog cleanup, or a SKU that was created by mistake).

Required scope: catalog:write

Query parameters

NameTypeRequiredDescription
forcebooleanNoTruthy values (any case): true, 1, yes, on. Falsy values: false, 0, no, off, or omitted. Anything else returns 400 validation_failed. When truthy, permanently removes the product. Default is soft delete.

Request: soft delete (default)

bash
curl -X DELETE https://api.thehumind.com/public/v1/products/api:SKU-123 \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 1a2b3c4d-5e6f-4789-9abc-def012345678"

Request: hard delete

bash
curl -X DELETE 'https://api.thehumind.com/public/v1/products/api:SKU-123?force=true' \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 1a2b3c4d-5e6f-4789-9abc-def012345678"

Response 204 No Content

No body.

Soft vs. hard delete

Soft delete is recommended for normal merchant operations: chat conversations that referenced the product stay coherent, you can un-archive in one PATCH, and downstream caches (vector index, dashboards) tear down cleanly via the archive cascade.

Hard delete (?force=true) is for catalog cleanup: typically test or sandbox products you want fully gone. Soft delete first triggers the archive routine; ?force=true then permanently removes the resource. Collections that referenced the product simply skip the missing reference; we don't cascade further.


Batch upsert

POST /products/batch accepts up to 500 products in a single call and returns a 207 Multi-Status response with one entry per item. Use this for the initial backfill of a catalog or any bulk update, it's far faster than 500 sequential POSTs.

Required scope: catalog:write

ConstraintValue
Max items per batch500
Max body size5 MB
BehaviourEach item is processed independently; one failure doesn't block the rest.

Request

bash
curl -X POST https://api.thehumind.com/public/v1/products/batch \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 4d3c2b1a-9f8e-4d7c-6b5a-4f3e2d1c0b9a" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "external_id": "SKU-123",
        "title": "Crème hydratante",
        "handle": "creme-hydratante",
        "default_language": "fr",
        "variants": [
          { "external_id": "SKU-123-50ML", "title": "50 ml", "price": 29.90, "currency": "EUR" }
        ]
      },
      {
        "external_id": "SKU-456",
        "title": "Sérum éclat",
        "handle": "serum-eclat",
        "default_language": "fr",
        "variants": [
          { "external_id": "SKU-456-30ML", "title": "30 ml", "price": 49.00, "currency": "EUR" }
        ]
      },
      {
        "external_id": "SKU-789",
        "title": "Bad data",
        "default_language": "fr"
      }
    ]
  }'

Response 207 Multi-Status

json
{
  "results": [
    {
      "external_id": "SKU-123",
      "status": "created",
      "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f"
    },
    {
      "external_id": "SKU-456",
      "status": "updated",
      "humind_id": "65f1cd9e8e7d4a2b1c3d4e60"
    },
    {
      "external_id": "SKU-789",
      "status": "failed",
      "error": {
        "code": "validation_failed",
        "message": "Invalid product payload.",
        "details": {
          "issues": [
            { "path": ["handle"], "message": "Required", "code": "invalid_type" }
          ]
        }
      }
    }
  ]
}
FieldTypeDescription
results[i].external_idstringThe external_id of the input item, echoed for matching.
results[i].statusstringOne of created, updated, failed.
results[i].humind_idstringPresent on success: the assigned 24-hex Humind id.
results[i].errorobjectPresent on failure. Same shape as a top-level error object.

Duplicate external_id within a batch

If the same external_id appears more than once in items, the first occurrence is processed and later duplicates come back with status: "failed" and code duplicate_external_id_in_batch. Deduplicate client-side before sending.

Wrapper or bare array

The body may be a JSON array as shown above, or wrapped: { "items": [ ... ] }. The API normalises both to the same results response.

Idempotency on batches

The Idempotency-Key applies to the whole batch. A retry of the same batch with the same key replays the entire 207 response, no items are processed twice. Generate a new UUID for a different batch.


Translations sub-resource

For products that already exist, you can patch a single locale without resending the full payload. Use the embedded translations field on POST / PUT / PATCH for the initial seed, then drive ongoing updates through these endpoints.

{lang} is a BCP 47 short tag, en, fr, de, pt-BR, zh-CN, etc. The validation regex is ^[a-z]{2}(-[A-Z]{2})?$. An invalid value returns 422 with code: invalid_lang_format.

Set or update a translation

PUT /products/{id}/translations/{lang} upserts the translation for a single locale. Fields you omit are left untouched on an update; on first write, they default to unset.

Required scope: catalog:write

FieldTypeDescription
titlestringLocalized display title.
descriptionstringLocalized plain-text description.
description_htmlstringLocalized HTML description. Sanitized, see HTML content.
handlestringLocalized URL-safe slug.
online_store_urlstringLocalized public URL on your storefront.
ingredientsstring[]Localized ingredients list.
metafieldsobject[]Per-language metafield overrides, matched by (namespace, key). Same format and semantics as translated metafields. Sending the property replaces the whole per-language list; [] clears it.

All fields are optional individually, but the body must contain at least one. An empty body returns 400 validation_failed.

Request

bash
curl -X PUT https://api.thehumind.com/public/v1/products/api:SKU-123/translations/en \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 5f6a7b8c-9d0e-4f1a-2b3c-4d5e6f7a8b9c" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Hydrating cream",
    "description": "A lightweight face cream.",
    "handle": "hydrating-cream",
    "online_store_url": "https://example.com/en/products/hydrating-cream"
  }'

Response 200 OK

Returns the full product object, the same shape as the create response, so you can see every locale currently stored, not just the one you wrote.

Remove a translation

DELETE /products/{id}/translations/{lang} removes the translation for a single locale. Idempotent: deleting a locale that doesn't exist is a no-op, not an error.

Required scope: catalog:write

Request

bash
curl -X DELETE https://api.thehumind.com/public/v1/products/api:SKU-123/translations/en \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 6a7b8c9d-0e1f-4a2b-3c4d-5e6f7a8b9c0d"

Response 204 No Content

No body.


Variants sub-resource

For products that already exist, you can manage a single variant without resending the full product. The variant is identified by its external_id in the URL path.

{variant_external_id} is the same identifier you set on the variant's external_id field, typically your SKU.

Set or update a variant

PUT /products/{id}/variants/{variant_external_id} upserts a single variant. The body matches the Variant schema, without external_id: that value is taken from the URL.

Required scope: catalog:write

Request

bash
curl -X PUT https://api.thehumind.com/public/v1/products/api:SKU-123/variants/SKU-123-50ML \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 7b8c9d0e-1f2a-4b3c-4d5e-6f7a8b9c0d1e" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "50 ml",
    "sku": "SKU-123-50ML",
    "price": 29.90,
    "compare_at_price": 34.90,
    "currency": "EUR",
    "available_for_sale": true,
    "inventory_quantity": 42
  }'

Response 200 OK

Returns the full product object so you can see the updated variant alongside the rest of the product.

Remove a variant

DELETE /products/{id}/variants/{variant_external_id} removes a single variant from the product. Idempotent: deleting a variant that doesn't exist is a no-op, not an error.

Required scope: catalog:write

A product needs at least one variant

You can't remove the last variant of a product, that would leave it without a price or currency. To fully retire a product, archive it instead.

Request

bash
curl -X DELETE https://api.thehumind.com/public/v1/products/api:SKU-123/variants/SKU-123-50ML \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 8c9d0e1f-2a3b-4c4d-5e6f-7a8b9c0d1e2f"

Response 204 No Content

No body.


Common errors

The product endpoints can return any of the standard HTTP statuses, but these are the ones you'll see most often:

StatusCodeWhenFix
401missing_credentials, invalid_key_format, invalid_key, revokedAuth header is missing, malformed, or the key is no longer active.See Authentication.
403insufficient_scopeKey lacks catalog:read (for GET) or catalog:write (for POST/PUT/PATCH/DELETE).Create a new key with the right scope.
404not_foundThe {id} doesn't match a product owned by your company.Confirm the external_id or humind_id. Cross-tenant lookups also return 404.
404variant_not_foundThe {variant_external_id} on a variant sub-resource call doesn't match any variant on this product.Confirm the variant's external_id. DELETE is idempotent, see notes there before retrying.
409idempotency_conflictSame Idempotency-Key reused with a different body.Generate a fresh UUID.
400validation_failedBody fails the Product schema. details points to the bad field.Fix the field and resend.
422invalid_lang_formatThe {lang} segment of a translation sub-resource URL doesn't match ^[a-z]{2}(-[A-Z]{2})?$.Use a BCP 47 short tag like en, fr, pt-BR.

Next

Released under the proprietary Humind license.