B2B Features
Customer prices, quote requests, cookie consent, and return requests
Customer prices throughout the purchase
Catalog, cart and checkout resolve the customer's active groups and currency before pricing. Explicit per-product price-list entries take precedence over the group's default percentage discount. If several eligible lists contain the same product, the lowest explicit price wins. Computed group discounts respect the merchant's margin floor and price-ending policy; manually entered prices remain merchant decisions. Eligible quantity tiers can lower that customer price, price rules apply next, and an active early-bird price may lower the result again.
Shipping thresholds and checkout review use the customer's current cart prices, including bundle totals. Do not calculate a customer price by subtracting a percentage in the template. Display the API's price and revalidate after changes.
Use the authenticated request-scoped client for the first HTML of product lists, details and recommendations. Disable shared response caching for these reads. A public singleton must never hold a visitor's auth token or cart session. The API hides prices for gated guests and rejects attempts to obtain them through cart or shipping calculations. Hidden prices must also hide quantity-tier prices.
Delivery-country rules must also be resolved in the first HTML. SDK 1.20 accepts
country: 'SK' when creating a request-scoped client, or client.setCountry('SK').
This default reaches catalog lists, details, featured products, collections,
recommendations and bundles. An explicit per-call country takes precedence.
The Next.js adapter reads behio_country (or countryCookieName) for each request.
Write that cookie only after cart.setDestination() succeeds and refresh the
server render. setCountry() changes catalog context only, never the cart.
Guest response cache keys must include currency and country; customer responses
must remain private. Do not replace server prices from a browser-wide price cache.
Variant priceFrom uses the same variant price-list entries, category rules,
cost guards and early-bird price as the detail. Variant availability uses only
the warehouses selected for the shop. Internal costs never leave the API.
Recommendations expose variantsOnly and priceFrom too; show a "from" label
for these families and use the returned price's own currency. Featured products,
collections and recommendations also apply the shop's FX rate and margin.
Before adding the first product or bundle, initialize an empty cart with the
visitor's validated display currency and known destination. A cookie can outlive
the previous cart. cart.setCurrency() and cart.setDestination() retain the
returned cart session, including when either call creates the cart. Stop the
purchase action if context initialization fails. Never add an item at the shop's
default price and silently repair its destination after displaying success.
Quote Requests
Allow B2B customers to request custom pricing. The contact email is the ownership gate for reading and accepting a quote (quotes carry contact PII and negotiated prices), so keep it available on the confirmation page.
const { data: quote, error } = await client.quotes.submit({
contactName: 'Jan Novak',
contactEmail: '[email protected]',
contactPhone: '+420123456789',
companyName: 'ACME Corp',
message: 'We need bulk pricing for Q3',
items: [
{ productId: 'product-1', quantity: 100, requestedPrice: 80 },
{ productId: 'product-2', quantity: 50 },
],
});
// quote.status: PENDING | QUOTED | ACCEPTED | REJECTED | EXPIREDCheck Status
const { data: status } = await client.quotes.getStatus('quote-id', '[email protected]');
// { id, status, quotedTotal, quotedCurrency, quotedNote, expiresAt, items, ... }Accept Quote
Only a quote in QUOTED status can be accepted; accepting twice is
idempotent and an expired quote flips to EXPIRED:
const { data: accepted } = await client.quotes.accept('quote-id', '[email protected]');Return Requests (EU Withdrawal Button)
EU Directive 2023/2673 (effective 19 June 2026) requires an easy, clearly
labelled online way to withdraw from the contract. Build a public /returns
page with this three-step flow. No customer login is needed; the email used
on the order is the ownership gate.
1. Look Up the Order
The customer enters their order number and email; you get back the internal ids and per-item returnable quantities:
const { data: order, error } = await client.returns.lookupOrder(
'2026-0042',
'[email protected]',
);
// order.items: [{ orderItemId, productName, quantity, returnableQuantity }]Cap each quantity input at returnableQuantity and skip items where it is 0
(already claimed by an active return).
2. Submit the Withdrawal
const { data: returnRequest, error } = await client.returns.submit({
orderId: order.orderId,
email: '[email protected]', // must match the order email
reason: 'CHANGED_MIND', // the 14-day EU default; see all reasons below
customerNote: 'Optional note',
items: [
{ orderItemId: 'item-id', productName: 'Product name', quantity: 1 },
],
});Reasons: CHANGED_MIND, DEFECTIVE, WRONG_ITEM, NOT_AS_DESCRIBED,
TOO_LATE, DUPLICATE_ORDER, OTHER. The platform immediately sends the
acknowledgement email the directive requires; show returnRequest.id to the
customer as their reference number.
3. Check Return Status
const { data: status, error } = await client.returns.getStatus(
'return-id',
'[email protected]',
);
// status.status: REQUESTED | APPROVED | SHIPPED_BACK | RECEIVED |
// REFUNDED | REJECTED | CLOSED
// also: refundAmount, refundedAt, returnTrackingNumber, itemsRun all three calls from Server Actions (or your backend) so the customer's email never appears in a URL.
Cookie Consent (GDPR)
The consent log is append-only (GDPR audit trail); get returns the latest
active consent and resolves to null when none was recorded yet. Since SDK
0.36.0 it calls the 200-always /consent/:visitorId/status endpoint, so a
first-time visitor no longer produces a 404 in the browser console.
// Record consent (visitorId = your anonymous cookie/fingerprint id, 8-128 chars)
await client.consent.record({
visitorId: 'visitor-uuid',
analytics: true,
marketing: false,
preferences: true,
});
// Get the latest active consent, data is null when none exists
const { data: consent } = await client.consent.get('visitor-uuid');
// Revoke (idempotent)
await client.consent.revoke('visitor-uuid');