Connect your own frontend

Run a storefront on your own stack, in Next.js, Laravel or anything else, as an API client of the platform. Covers the server-side key, the market and channel lookups, batched prices, buyer sessions and tenant-safe caching.

This guide is the alternative to the Theme path for the developer whose storefront is not a Nuxt Theme: a Next.js, Laravel, Symfony or Rails shop that keeps running on your own infrastructure and reads its markets, prices and buyers from the revenexx API. By the end your server can resolve a market and a channel, price a whole listing in one call, hand a buyer a session, and cache all of it without one tenant seeing another's data.

It stays at the connection layer, and it does not cover product listings: read those from the routes in Products. Checkout, faceted search and storefront performance are not covered here. If you have not yet decided between a Theme and an API client, read Bring an existing frontend and Frontend stacks first. Choosing this path means hosting, sessions, tenancy and deploys stay yours, and page composition and the theme marketplace are not available to you.

Prerequisites

  • A tenant slug, the store you are connecting to. Prerequisites shows where to find it.
  • An API key. Section 1 creates it (create an API key; which scope covers which app); the examples below read it from the environment.
  • The code of a market and of a channel already set up on the tenant, such as de and web.
  • A server. Every call below carries a secret. None of it may run in a browser.

If you start from an empty tenant, see Markets and Channels first.

Every call sends the tenant in X-Revenexx-Tenant and the key in X-Revenexx-Api-Key. Once you have created the key in section 1, the examples read both from the environment:

Set REVENEXX_TENANT to your tenant slug.

Environment
export REVENEXX_TENANT="<TENANT_SLUG>"
export REVENEXX_API_KEY="rvxk_..."

Quick start

The sequence a server runs, with the section that explains each step.

  1. Key: create a server-side key per tenant in Cockpit (section 1).
  2. Market and channel: GET /v1/markets?code=<code> once to get the market's id, then GET /v1/markets/{id}/context and GET /v1/channels/context?channel=<code> (section 2).
  3. Prices: POST /v1/prices/resolve with up to 200 items per call (section 3).
  4. Buyers: sign them in from your server only, as in Sign a buyer in from your server (section 4).
  5. Cache: put the tenant, and everything else the answer depends on, in every cache key (section 5).

1. Create a server-side key per tenant

Create the key in Cockpit under API keys, as described in API keys and create an API key. Three rules for a storefront:

  • One key per tenant, per environment. A key identifies exactly one tenant, so a frontend that serves several tenants holds several keys and picks one per request. The tenant header alone does not choose access. A key sent with another tenant's slug is refused with 403.
  • Scope it to what the storefront calls. Scopes are capability keys or namespaces, for example markets.*, channels.*, prices.*, products.* and customers.auth.* (which scope covers which app). A namespace such as orders.* includes writes, so name the operations you need rather than granting a whole namespace where the job only reads.
  • Keep it on the server. The key has standing access to everything in its scopes for every buyer in the tenant. Load it from a secret manager or an environment variable, never from code that ships to a browser.

A call outside the key's scopes answers 403. A missing or revoked key answers 401.

2. Resolve the market and the channel

A market decides currency, locales and tax. A channel decides which assortment a buyer sees. Resolve both at startup or behind a short cache, not on every page render.

The market. The startup route takes a uuid only (as of 2026-09). Handing it a market code such as de answers 400, although other market routes accept a code. So look the id up first, by code:

Request
curl "https://api.revenexx.com/v1/markets?code=de" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY"

The answer is { "items": [...], "page": {...} }. Read the id of the row, and check its code really is the one you asked for. A filter on a field the list does not have is dropped and the call answers 200 with the unfiltered list. To find the tenant's default market, filter with is_default=true. Then ask for the context:

Request
curl "https://api.revenexx.com/v1/markets/<MARKET_ID>/context" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY"

The answer holds the market, its locales, currencies and tax classes, the locale to render in and whether prices are net or gross. Read readiness before you trust the rest: readiness.ready is false when the market cannot yet serve anybody, for example when it has no currency or no tax class. locales being empty is a real answer, so read default_locale.

The channel. Name the channel in the query string:

Request
curl "https://api.revenexx.com/v1/channels/context?channel=web" \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY"

Use ?channel=, not a header. As of 2026-09, a channel header is accepted by the channel service but does not get through the gateway, so it does nothing. Send the query parameter every time. Without it the route falls back to the channel flagged as default.

This route never answers an error for a channel it does not know. It answers 200 with resolved: false and a reason, for example unknown_channel, no_default_channel or channel_required. Check resolved before you render, or you will show a shop with no assortment and no error to explain it.

Carry the market on later calls. The one header that reaches the services from you is X-Revenexx-Market, with the market code. Send it on the calls that depend on the market, as the price call in the next section does. Do not rely on a locale header: as of 2026-09 it does not reach the services. Pass locale choices as parameters where the route offers them.

3. Price in batches of 200

Price a whole listing or cart with one call. <CONTACT_ID> is the contact.id from the buyer's sign-in answer, and <CHANNEL_ID> is the channel.id from the channel context in step 2:

Request
curl -X POST https://api.revenexx.com/v1/prices/resolve \
  -H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
  -H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
  -H "X-Revenexx-Market: de" \
  -H "Content-Type: application/json" \
  -d '{
        "currency": "EUR",
        "contact_id": "<CONTACT_ID>",
        "channel_id": "<CHANNEL_ID>",
        "items": [
          { "sku": "ACME-4711-BLK", "quantity": 12 },
          { "sku": "ACME-9001", "quantity": 1 }
        ]
      }'

The answer holds a prices array with one entry per item, in request order.

  • More than 200 items is a 400. So is an empty items list, or an id that is not a uuid, or a market header that is not a market code. Chunk the list yourself, 200 at a time, and put the answers back in order. A 429 means the request was not processed: back off as in Rate limits.
  • Send currency explicitly. A list in another currency does not price the item, and nothing is converted. A wrong currency is not an error either. It comes back as no price.
  • Pass the channel. Send channel_id, the uuid from channel.id in the channel context, so a price list assigned to that channel applies. Without it the channel's list is never considered.
  • Pass the buyer when you know them. A signed-in buyer's contact_id (and organization_id) selects contract prices. Without them the buyer gets the open price, or none at all if the list needs a signed-in buyer.
  • Never show a price of zero. An item nobody priced comes back with unit_price: null, on_request: true and an on_request_reason such as not_priced, on_request_entry, anonymous_denied or no_identity. Render "price on request", and treat a null as a state, not as a number.
Node
// Price any number of items, 200 per call, answers kept in order.
async function priceAll(items, { market, currency, contactId, channelId }) {
  const out = []
  for (let i = 0; i < items.length; i += 200) {
    const res = await fetch('https://api.revenexx.com/v1/prices/resolve', {
      method: 'POST',
      headers: {
        'X-Revenexx-Tenant': process.env.REVENEXX_TENANT,
        'X-Revenexx-Api-Key': process.env.REVENEXX_API_KEY,
        'X-Revenexx-Market': market,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ currency, contact_id: contactId, channel_id: channelId, items: items.slice(i, i + 200) }),
    })
    if (!res.ok) throw new Error(`prices/resolve ${res.status}`)
    out.push(...(await res.json()).prices)
  }
  return out
}

The full resolution order, and how a price list is picked, is on Price lists.

4. Sign buyers in through your own server

A buyer never talks to the platform directly. Your server calls the sign-in routes with the key, keeps the session it gets back, and gives the browser only a cookie that points at it.

  • The pattern is a backend for your frontend. Your server registers and signs the buyer in with POST /v1/customers/auth/login, stores the session on the server, and sets an httpOnly, Secure cookie that names it.
  • The API key is the credential, not the buyer's token. There is no bearer token for a buyer, and there is no refresh call. A session lasts until it expires or is revoked. Then the buyer signs in again.
  • Check the session before you act for the buyer. Ask POST /v1/customers/auth/me with the user_id and session_id from the sign-in answer before an account page, a checkout or an order. A 401 means drop the cookie and sign in again.
  • The key must cover the sign-in routes. Its scopes need customers.auth.*, or the individual customers.auth.… operations you call.

Registration, sign-in, the second factor, sign-out and every 401 and 403 are covered step by step in Sign a buyer in from your server. The routes are for a trusted server only, because the sign-in answer carries a session secret. See Storefront accounts.

5. Put the tenant in every cache key

A frontend that serves more than one tenant, or one tenant in more than one market, caches wrong data the moment a key leaves something out. The rule: a cache key holds everything the answer depends on.

What you cacheThe key must include
Market contextTenant, market id
Channel contextTenant, channel code
Prices for a listingTenant, market, channel, currency, and the buyer (contact_id or organization_id), or bypass the cache for a signed-in buyer
Anything derived from a buyer's sessionNever in a shared cache

Two things to know about the platform's side:

  • The gateway caches some reads by itself (as of 2026-09). A cacheable response carries X-Cache: HIT or MISS. The market list and market context are cached by tenant and path. The price call and the channel context are not cached at the gateway, so keeping them fast is your job, with a short lifetime.
  • Its cache is not yours. The gateway keys its cache on the tenant, the route, the path and the query string. It does not cross tenants, and it adds your market or channel to a key only on routes that vary on them. Your own cache gets no such help.

A price is the most dangerous thing to cache wrongly, because the wrong one shows a buyer another buyer's contract price. When in doubt, key by buyer, or skip the cache for signed-in buyers.

Troubleshooting

  • 400 on the market context. You sent a market code. Use the id.
  • The channel context answers 200, but nothing renders. Check resolved and reason. An unknown channel is not an error.
  • The channel seems to be ignored. You sent it as a header. Send ?channel=.
  • 400 from prices/resolve. More than 200 items, an empty items list, an id that is not a uuid, or a market header that is not a code.
  • Every item comes back "on request". Check currency against the price lists, and that the buyer is identified if the list needs a signed-in buyer. If the answer's tax.resolved is false, name the market in X-Revenexx-Market.
  • 403 on any call. Read the message. api key not valid for this tenant means the key and the tenant header disagree. api key scope does not permit this capability means the key lacks a scope.
  • A buyer sees another buyer's price. A cache key is missing the buyer, or the price call ran without the buyer's ids. Fix the key.

Where to go next

Was this page helpful?