Behio Storefront SDK
Advanced

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 | EXPIRED

Check 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, items

Run all three calls from Server Actions (or your backend) so the customer's email never appears in a URL.

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');

On this page