B2B / Customer Groups

Group-Specific Fields

Makaira supports customer group-specific field overrides for B2B scenarios, multi-channel setups, and personalized pricing.

Overview

Any field can have a group-specific override using the pattern:

groups.<groupname>.<fieldname>

When a GROUP constraint is set in the API request (query.group: "<groupname>"), Makaira applies the group-specific values. Group names are case-sensitive and must match the constraint value exactly.

How It Works

Group overrides behave differently for visibility fields than for value fields. This is the most important detail of the feature — please read carefully before designing your group strategy.

Value fields (price, custom fields, ...) — full override

For non-visibility fields, the group value fully replaces the root value when the matching GROUP constraint is set.

Root: price = 99.99
Group "wholesale": price = 79.99
→ With GROUP=wholesale → returned price is 79.99
→ Without GROUP constraint → returned price is 99.99

Visibility fields (active, searchable, hideOnLists, onstock) — gate, not toggle

Visibility fields work differently. The root field is always evaluated as a hard gate, and the group override can only further restrict visibility — it can never unlock a product that is hidden at the root level.

RootGroup overrideResult for that group
active: truenot setvisible
active: trueactive: falsehidden
active: falsenot sethidden
active: falseactive: truestill hidden
active: falseactive: falsehidden

The same logic applies to searchable, hideOnLists, and onstock.

❗️

"Hidden by default, visible per group" is not supported

A common request is "make products inactive by default, then enable them for specific customer groups". This does not work with active, searchable, hideOnLists, or onstock. The root field has to be in the permissive state (active: true, searchable: true, onstock: true, hideOnLists: false) for any group to ever see the product.

If you need that semantic, model it the inverse way: keep the root permissive, and explicitly set the group override to false for every group that should not see the product. Or use a custom keyword field plus a stream filter to control inclusion outside the visibility-field path.

Commonly Overridden Fields

Field PatternDescription
groups.<group>.activeGroup-specific activation
groups.<group>.hiddenGroup-specific visibility
groups.<group>.hideOnListsGroup-specific list visibility
groups.<group>.searchableGroup-specific searchability
groups.<group>.onstockGroup-specific stock status
groups.<group>.stockGroup-specific stock quantity
groups.<group>.priceGroup-specific pricing

Import Example

{
  "id": "product-001",
  "type": "product",
  "active": true,
  "onstock": true,
  "price": 99.99,
  "stock": 100,
  "groups": {
    "wholesale": {
      "active": true,
      "onstock": true,
      "price": 79.99,
      "stock": 1000,
      "hideOnLists": false
    },
    "retail": {
      "active": true,
      "onstock": true,
      "price": 99.99,
      "stock": 50
    },
    "vip": {
      "active": true,
      "price": 69.99,
      "hideOnLists": false
    },
    "internal": {
      "active": false
    }
  }
}

API Request with Group

To use group-specific values, include the GROUP constraint:

{
  "searchPhrase": "t-shirt",
  "isSearch": true,
  "constraints": {
    "query.shop_id": "shop1",
    "query.language": "de",
    "query.group": "wholesale"
  }
}

Use Cases

B2B Pricing

Different prices for different customer tiers:

{
  "id": "product-001",
  "price": 99.99,
  "groups": {
    "wholesale": {"price": 79.99},
    "distributor": {"price": 59.99},
    "retail": {"price": 99.99}
  }
}

Group-Specific Visibility

Hide products from certain groups:

{
  "id": "product-001",
  "active": true,
  "groups": {
    "consumer": {"active": true},
    "b2b_only": {"active": true},
    "internal": {"active": false}
  }
}

Group-Specific Stock

Different stock pools per channel:

{
  "id": "product-001",
  "stock": 100,
  "onstock": true,
  "groups": {
    "online": {"stock": 50, "onstock": true},
    "store_pickup": {"stock": 25, "onstock": true},
    "wholesale": {"stock": 500, "onstock": true}
  }
}

Regional Availability

Products available only in certain regions:

{
  "id": "product-001",
  "active": true,
  "groups": {
    "de": {"active": true},
    "at": {"active": true},
    "ch": {"active": false}
  }
}

Visibility Filter Logic

This section is the precise rule behind the gate-not-toggle callout above.

When a group is set, the search query for visibility fields is built like this (pseudo-code, see ActiveFilter, SearchableFilter, HideOnListsFilter, OnstockFilter in the API code):

must:        root field is in the permissive state   # always required
must_not:    groups.<group>.<field> is in the restrictive state   # only added when GROUP is set

For active, searchable, onstock the permissive state is true; for hideOnLists it is false. The group override can only add an additional must_not to hide the product for that group — it cannot remove the root must.

Logic for active field:

  1. Root active must be true — otherwise the product is hidden for everyone, regardless of group.
  2. If GROUP is set and groups.<group>.active is false, the product is additionally hidden for that group.
  3. If GROUP is set and groups.<group>.active is true or missing, the product follows the root value.

Examples:

Root: active = true,  Group "wholesale": active = false
→ Product is HIDDEN for wholesale customers, visible for everyone else.

Root: active = false, Group "wholesale": active = true
→ Product is HIDDEN for wholesale customers too — root gate wins.

Stock Ranking with Groups

When "Out of stock at the end" is enabled in Ranking Mix:

  1. Makaira checks for groups.<group>.stock
  2. If exists, uses group-specific stock for ranking
  3. If not, falls back to root stock field
{
  "id": "product-001",
  "stock": 100,
  "groups": {
    "wholesale": {"stock": 0}
  }
}

For wholesale customers, this product would be pushed to the bottom of results (stock = 0).

Custom Group Fields

You can add any custom field as a group override:

{
  "id": "product-001",
  "delivery_days_int": 3,
  "min_order_quantity_int": 1,
  "groups": {
    "wholesale": {
      "delivery_days_int": 1,
      "min_order_quantity_int": 10
    },
    "dropship": {
      "delivery_days_int": 5,
      "min_order_quantity_int": 1
    }
  }
}

Best Practices

  1. Define all groups consistently - Use the same group names across all products.

  2. Set defaults at root level - Always provide root field values as fallback.

  3. Only override what changes - Don't duplicate values that are the same as root.

  4. Document your groups - Maintain a list of group names and their purposes.

  5. Test group visibility - Verify that group-specific filtering works as expected.

Limitations

  • Visibility fields are gated by the root, not toggled. active, searchable, hideOnLists, onstock cannot be unlocked by a group override — see Visibility fields: gate, not toggle.
  • Group overrides are only applied when the GROUP constraint is set. Without query.group in the request, every consumer sees the root values.
  • Group names are case-sensitive and must match the constraint value exactly.
  • Group fields must keep the same type as the root field. A price override has to stay numeric, a string override has to stay a string.
  • The groups object must exist on the product and on every variant of that product, with consistent sub-fields across all groups (see the NDJSON guide). Inconsistent sub-fields lead to broken Elasticsearch mappings.
  • Nested attribute structures (attributeStr, attributeInt, attributeFloat, attributes) cannot be overridden via groups. Group overrides only apply to flat fields.
  • No partial merging. A group override replaces the root value as a whole — there is no per-sub-field merge for object-typed fields.