Skip to content

Collections

Collections are named groups of products. The AI assistant uses them to narrow recommendations to a subset of your catalog, "show me bestsellers", "what's in the summer 2026 drop", "anything from the gift guide". Push your merchandising structure once and the assistant will respect it everywhere it surfaces products.

The endpoints here cover create, read, update, replace, archive, and batch operations on collections, including their product memberships and embedded translations.

The Collection object

FieldTypeRequiredDescription
external_idstringYes (on create)Your unique identifier for this collection. Free-form, must be unique per company. Used as the upsert key, posting again with the same value updates the existing collection.
humind_idstringReturned only24-char hex ObjectId Humind assigns. Stable for the lifetime of the collection.
titlestringYesDisplay title in default_language.
handlestringNoURL-safe slug, lowercase, hyphen-separated. Unique per company when set — assigning a handle that already belongs to a different collection (matched on a different external_id) returns 409 handle_already_used. Upserts on the same external_id keep their existing handle unchanged.
descriptionstringNoPlain-text description.
description_htmlstringNoHTML description. Sanitized server-side, see HTML content.
status'active' | 'archived'NoDefaults to active. archived hides the collection from the assistant.
default_languagestringNoISO 639-1 code for the language of the top-level fields.
source'public-api' | 'shopify' | 'supersmart'NoDefaults to public-api. Tag the collection with the integration that owns this data — useful when you're pushing data through this API that you also maintain in another system. The same value drives the upsert lookup, so re-posting with the same source + external_id updates the same row. Any other value is rejected with a 400 validation_failed error on source.
productsobject[]NoProduct memberships. See Linking products by external_id.
translationsobject[]NoPer-locale overrides. See Translations.
created_atISO 8601Returned onlyCreation timestamp (UTC).
updated_atISO 8601Returned onlyLast modification timestamp (UTC).

Product membership

Each entry in products describes one product that belongs to the collection.

FieldTypeRequiredDescription
external_idstringYesThe external_id of an existing product. The server resolves it to the matching humind_id at write time. See Linking products by external_id.
include_all_variantsbooleanNoDefaults to true. When true, every variant of the product is part of the collection.
included_variantsstring[]NoRequired if include_all_variants is false. List of variant external_ids to include. Variants not listed are excluded from the collection.

Translations

translations is a list of per-locale overrides keyed by language:

json
{
  "default_language": "fr",
  "title": "Nos meilleures ventes",
  "description": "Les produits les plus aimés de la saison.",
  "translations": [
    {
      "language": "en",
      "title": "Bestsellers",
      "description": "Our most-loved products this season."
    }
  ]
}

Per-locale fields you can override:

FieldType
titlestring
descriptionstring
description_htmlstring

Locales not listed in translations fall back to the top-level (default_language) value.

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

HTML content

The description_html field accepts HTML. Humind sanitizes HTML server-side before storing it, using a fixed allowlist:

  • Tags preserved: p, a, br, hr, em, strong, b, i, u, ul, ol, li, h1h6, 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 /collections creates a new collection, 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/collections \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 3f1a8d92-7c4b-4e6f-b2a1-d5e9c8f7a3b4" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "col-bestsellers",
    "handle": "bestsellers",
    "title": "Bestsellers",
    "description": "Our most-loved products this season.",
    "status": "active",
    "default_language": "en",
    "products": [
      { "external_id": "SKU-123", "include_all_variants": true },
      { "external_id": "SKU-456", "include_all_variants": false, "included_variants": ["SKU-456-A"] }
    ],
    "translations": [
      {
        "language": "fr",
        "title": "Nos meilleures ventes",
        "description": "Les produits les plus aimés de la saison."
      }
    ]
  }'

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

json
{
  "external_id": "col-bestsellers",
  "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f",
  "handle": "bestsellers",
  "title": "Bestsellers",
  "description": "Our most-loved products this season.",
  "status": "active",
  "default_language": "en",
  "products": [
    { "external_id": "SKU-123", "include_all_variants": true },
    { "external_id": "SKU-456", "include_all_variants": false, "included_variants": ["SKU-456-A"] }
  ],
  "translations": [
    {
      "language": "fr",
      "title": "Nos meilleures ventes",
      "description": "Les produits les plus aimés de la saison."
    }
  ],
  "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 collection (first time the API saw this external_id). A 200 OK means the collection already existed and was updated in place. Both are success.


List collections

GET /collections returns the collections 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 (1100). Defaults to 50.
handlestringFilter to a single collection by exact handle match.

Request

bash
curl https://api.thehumind.com/public/v1/collections?limit=50 \
  -H "Authorization: Bearer hmd_live_..."

To fetch the next page:

bash
curl "https://api.thehumind.com/public/v1/collections?limit=50&cursor=eyJpZCI6IjY1ZjEuLi4ifQ" \
  -H "Authorization: Bearer hmd_live_..."

Response 200 OK

json
{
  "data": [
    {
      "external_id": "col-bestsellers",
      "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f",
      "handle": "bestsellers",
      "title": "Bestsellers",
      "status": "active",
      "default_language": "en",
      "products": [
        { "external_id": "SKU-123", "include_all_variants": true }
      ],
      "created_at": "2026-04-25T14:30:00Z",
      "updated_at": "2026-04-25T14:30:00Z"
    }
  ],
  "next_cursor": "eyJpZCI6IjY1ZjFjZDllOGU3ZDRhMmIxYzNkNGU2MCJ9"
}

When next_cursor is null or missing, you've reached the last page.


Retrieve a collection

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

Required scope: catalog:read

Request, by external_id

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

Request, by humind_id

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

Response 200 OK

Same shape as the create response.


Replace a collection

PUT /collections/{id} performs a full replace: the request body becomes the entire collection. Fields you don't include are reset to their defaults (or unset where applicable). The products array you send replaces the current membership in full.

Use this when you have a complete representation of the collection 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/collections/api:col-bestsellers \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 7a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "col-bestsellers",
    "title": "Bestsellers, April 2026",
    "handle": "bestsellers",
    "status": "active",
    "default_language": "en",
    "products": [
      { "external_id": "SKU-123", "include_all_variants": true },
      { "external_id": "SKU-789", "include_all_variants": true }
    ]
  }'

Response 200 OK

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

Replace is destructive

PUT removes any field, product membership, or translation not present in the body. To add or remove a single product, use PATCH instead.


Update a collection

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

When you send a products array in PATCH, it replaces the membership list as a whole, there's no per-entry merge. To add a product, send the existing list plus the new entry. To remove one, send the existing list minus that entry.

Required scope: catalog:write

Request, rename without touching membership

bash
curl -X PATCH https://api.thehumind.com/public/v1/collections/api:col-bestsellers \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Bestsellers, April 2026"
  }'

Request, add a product to the collection

bash
curl -X PATCH https://api.thehumind.com/public/v1/collections/api:col-bestsellers \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 2c3d4e5f-6789-4abc-9def-0123456789ab" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      { "external_id": "SKU-123", "include_all_variants": true },
      { "external_id": "SKU-456", "include_all_variants": false, "included_variants": ["SKU-456-A"] },
      { "external_id": "SKU-999", "include_all_variants": true }
    ]
  }'

Response 200 OK

Returns the full updated collection object.


Archive (or delete) a collection

DELETE /collections/{id} performs a soft delete by default: the collection's status is set to archived and the assistant stops using it for recommendations. 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 collection 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 collection gone (e.g. test catalog cleanup, or a collection 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 collection. Default is soft delete.

Request — soft delete (default)

bash
curl -X DELETE https://api.thehumind.com/public/v1/collections/api:bestsellers \
  -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/collections/api:bestsellers?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: the products inside the collection stay intact, you can un-archive in one PATCH, and chat history that grounded recommendations on this collection stays coherent.

Hard delete (?force=true) is for catalog cleanup — typically a test or sandbox collection you want fully gone. Soft delete first triggers the archive routine; ?force=true then permanently removes the resource. Products that were grouped by this collection are not affected; only the grouping itself disappears.


Batch upsert

POST /collections/batch accepts up to 500 collections in a single call and returns a 207 Multi-Status response with one entry per item. Use this for the initial sync of your merchandising structure or any bulk update.

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/collections/batch \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 4d3c2b1a-9f8e-4d7c-6b5a-4f3e2d1c0b9a" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "external_id": "col-bestsellers",
        "title": "Bestsellers",
        "default_language": "en",
        "products": [
          { "external_id": "SKU-123", "include_all_variants": true }
        ]
      },
      {
        "external_id": "col-new-arrivals",
        "title": "New arrivals",
        "default_language": "en",
        "products": [
          { "external_id": "SKU-456", "include_all_variants": true }
        ]
      },
      {
        "external_id": "col-bad",
        "default_language": "en"
      }
    ]
  }'

Response 207 Multi-Status

json
{
  "results": [
    {
      "external_id": "col-bestsellers",
      "status": "created",
      "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f"
    },
    {
      "external_id": "col-new-arrivals",
      "status": "updated",
      "humind_id": "65f1cd9e8e7d4a2b1c3d4e60",
      "unresolved_products": ["SKU-456"]
    },
    {
      "external_id": "col-bad",
      "status": "failed",
      "error": {
        "code": "validation_failed",
        "message": "Invalid collection payload.",
        "details": {
          "issues": [
            { "path": ["title"], "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].unresolved_productsstring[]Optional. Lists product external_ids that didn't match any product owned by your company at write time. Not an error. See below.
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.


Linking products by external_id

When you push a collection, each entry in products references a product by your external_id, not by humind_id. The server resolves the reference at write time:

  • If the external_id matches a product owned by your company, the membership is recorded with the corresponding humind_id under the hood.
  • If the external_id does not match any product, the membership is still recorded, but the unmatched external_id is reported back in unresolved_products on the response. This is a warning, not an error: the request succeeds.

The collection automatically picks up unresolved products as soon as you push them. You don't need to re-PATCH the collection, the next time you create the missing product, Humind links it back to the collection.

Push products before collections

The cleanest order is: push your products first, then your collections. That avoids unresolved_products entirely. If you can't (e.g. you're rebuilding both at once), it's still safe, collections and products converge once both sides are in.

The single-resource endpoints (POST, PUT, PATCH /collections/{id}) follow the same rule. The response body includes unresolved_products: [...] whenever any entry didn't resolve, top-level alongside the collection object:

json
{
  "external_id": "col-bestsellers",
  "humind_id": "65f1ab9c8e7d4a2b1c3d4e5f",
  "products": [
    { "external_id": "SKU-123", "include_all_variants": true },
    { "external_id": "SKU-DOES-NOT-EXIST-YET", "include_all_variants": true }
  ],
  "unresolved_products": ["SKU-DOES-NOT-EXIST-YET"]
}

Translations sub-resource

For collections 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 /collections/{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.

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/collections/api:col-bestsellers/translations/fr \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 5f6a7b8c-9d0e-4f1a-2b3c-4d5e6f7a8b9c" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Nos meilleures ventes",
    "description": "Les produits les plus aimés de la saison."
  }'

Response 200 OK

Returns the full collection 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 /collections/{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/collections/api:col-bestsellers/translations/fr \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 6a7b8c9d-0e1f-4a2b-3c4d-5e6f7a8b9c0d"

Response 204 No Content

No body.


Common errors

The collection 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 collection owned by your company.Confirm the external_id or humind_id. Cross-tenant lookups also return 404.
409idempotency_conflictSame Idempotency-Key reused with a different body.Generate a fresh UUID.
409handle_already_usedThe submitted handle is already used by another collection in this company (matched on a different external_id).Pick a unique handle, or PATCH the existing collection on its external_id instead of creating a new one.
400validation_failedBody fails the Collection 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.

unresolved_products is not an error

You'll see this field on 200/201/207 responses when at least one entry in products[] couldn't be matched to an existing product. It's a heads-up, not a failure, the rest of the request was applied normally and Humind links the missing references the moment those products show up.

Next

  • Products: push the products that collections reference.
  • Knowledge: push FAQs, articles, and webpages the assistant grounds answers on.
  • Errors: full error code reference.

Released under the proprietary Humind license.