Developer docs menu

Customer tokens and B2B pricing

Sign a logged-in buyer in with a customer token, so the widget shows them their own agreed prices instead of your list prices. Node, Python and PHP.

This guide shows how to tell the widget which signed-in buyer is looking, so trade customers see their own price list, discounts and past quotes. You sign a short customer token on your server and pass it to the widget; a buyer cannot forge it.

Before you begin

  • A website where buyers sign in, with server-side code you control.
  • At least one price list assigned to a customer, under Customers in the console (on plans that include price lists).

How a customer token is built

base64url({"email":"buyer@acme.example","exp":1790000000}).base64url(HMAC-SHA256)

A token is two base64url parts separated by a dot: the claims, then an HMAC-SHA256 of the first part made with your shop’s shared secret. It is deliberately not a JWT: there is no algorithm field to negotiate, no library to choose, and it is short enough to sit in an HTML attribute.

ClaimTypeRequiredDescription
emailstringYesThe buyer’s e-mail address, trimmed and lower-cased before signing. Sign it exactly as you would look it up.
expintegerYesExpiry, in Unix seconds.

1. Create the shared secret

  1. In the console, open Customers and choose the Logged-in buyers tab.
  2. Press Create the secret. The secret starts with cts_ and is shown once: store it where you keep your other server secrets.

2. Sign a token on your server

Sign tokens on your server, never in the browser: a secret in page source is not a secret.

// Node.js
import { createHmac } from 'node:crypto';

const b64url = (buf) => Buffer.from(buf).toString('base64url');

export function customerToken(secret, email, ttlSeconds = 3600) {
  const claims = JSON.stringify({ email: email.trim().toLowerCase(), exp: Math.floor(Date.now() / 1000) + ttlSeconds });
  const body = b64url(claims);
  return `${body}.${b64url(createHmac('sha256', secret).update(body).digest())}`;
}
# Python
import base64, hashlib, hmac, json, time

def b64url(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()

def customer_token(secret: str, email: str, ttl_seconds: int = 3600) -> str:
    claims = json.dumps({"email": email.strip().lower(), "exp": int(time.time()) + ttl_seconds},
                        separators=(",", ":"))
    body = b64url(claims.encode())
    return f"{body}.{b64url(hmac.new(secret.encode(), body.encode(), hashlib.sha256).digest())}"
// PHP
function quotometry_customer_token(string $secret, string $email, int $ttl = 3600): string {
    $b64url = fn(string $raw) => rtrim(strtr(base64_encode($raw), '+/', '-_'), '=');
    $claims = json_encode(['email' => strtolower(trim($email)), 'exp' => time() + $ttl]);
    $body = $b64url($claims);
    return $body . '.' . $b64url(hash_hmac('sha256', $body, $secret, true));
}

A token lasts as long as you say, up to 30 days. An hour is a sensible default for a page a signed-in customer reloads. Mint a fresh token on each page render: it costs a few microseconds.

3. Pass it to the widget

<quotometry-quote widget="wgt_live_…" customer-token="<?= $token ?>"></quotometry-quote>

The buyer now sees their price list, their agreed discounts and their own past quotes. Their e-mail address is filled in for them, and the quote is attached to their customer record.

If the buyer signs in after the widget has loaded, call setCustomer({ token }) on the element instead of reloading the page. See the widget reference.

No back end to sign with?

POST /v1/customers/token on the merchant API mints a token for you. It exists for shops on a hosted storefront that cannot run server code, and for testing what a given customer sees: the Logged-in buyers tab uses it to make a test token that is valid for 24 hours.

It is not a replacement for signing. Calling it needs merchant credentials, which must never be in a browser your buyers use.

The weaker alternative: an e-mail claim

<quotometry-quote widget="wgt_live_…" customer-email="buyer@acme.example"></quotometry-quote>

customer-email is a claim, not proof: anyone can change it in the browser’s developer tools. It is fine for filling in a form, and not fine for granting a discount. When a token and an e-mail claim disagree, the token wins.

What a buyer is told

Nothing about how their price was reached. A price list applies as a discount line that the buyer can see (discount.price_list:<id>), and that is all: not the list’s name, not its percentage table, and not what anyone else pays.

Next steps

  • Widget reference: the customer-token attribute and setCustomer().
  • Cart recipes: verify a cart hand-off with the same shared secret.
  • Concepts: what a buyer-facing payload never contains.