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.
| Claim | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The buyer’s e-mail address, trimmed and lower-cased before signing. Sign it exactly as you would look it up. |
exp | integer | Yes | Expiry, in Unix seconds. |
1. Create the shared secret
- In the console, open Customers and choose the Logged-in buyers tab.
- 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-tokenattribute andsetCustomer(). - Cart recipes: verify a cart hand-off with the same shared secret.
- Concepts: what a buyer-facing payload never contains.