Cart recipes
Add a Quotometry quote to a cart you already run, in WooCommerce, Shopify or your own back end, without the price ever travelling through the browser.
This guide shows how to turn a finished quote into a line in a cart you already have, and then into an order. The pattern is the same for every cart: verify the quote id, read the price from the API on your server, and convert the quote when the buyer pays.
Before you begin
- A widget whose checkout mode hands the quote to your cart.
- A secret key (
sk_live_…orsk_test_…) from Developers → API keys, with scope to read quotes (quotes:read) and to create orders (orders:write). Keep it on your server. - Your shop’s shared secret, from Customers → Logged-in buyers, to check hand-off signatures.
How a cart hand-off works
- The buyer configures a part in the widget and presses your cart button.
- The widget fires
quotometry:cart-addwith{ quote_id, number, totals, signature }. - Your front end posts the
quote_idand thesignatureto your own back end. - Your back end checks the signature, fetches the quote from the Quotometry API with your secret key, reads the authoritative total, and adds a line to the cart at that price.
- At checkout, your back end converts the quote into an order.
Steps 4 and 5 run on your server. The secret key never reaches a browser.
Is this quote yours?
The price is safe without a signature, because you read it from the API. What is not safe without one is whose quote you are adding. A quote id is a string in an address bar: a visitor who pastes somebody else’s would get somebody else’s part, at somebody else’s price list, in their own basket.
So Quotometry signs the quote id it hands you:
v1.<expiry as unix seconds>.<base64url HMAC-SHA256>
The HMAC is taken over quotometry.cart.v1:<quote id>:<expiry> with your shop’s shared secret, the same
secret you use to sign customer tokens. The signature arrives as signature in the
quotometry:cart-add event, and as quotometry_sig in the query string of a redirect hand-off. It is valid for
one hour.
// Node.js, no dependencies
import { createHmac, timingSafeEqual } from 'node:crypto';
function handoffOk(quoteId, signature, secret) {
const [version, exp, mac] = String(signature).split('.');
if (version !== 'v1' || !/^\d+$/.test(exp ?? '')) return false;
const expected = createHmac('sha256', secret)
.update(`quotometry.cart.v1:${quoteId}:${exp}`)
.digest('base64url');
const a = Buffer.from(expected);
const b = Buffer.from(String(mac));
return a.length === b.length && timingSafeEqual(a, b) && Number(exp) >= Math.floor(Date.now() / 1000);
}
<?php
function quotometry_handoff_ok(string $quote_id, string $signature, string $secret): bool {
$parts = explode('.', $signature);
if (count($parts) !== 3 || $parts[0] !== 'v1' || !ctype_digit($parts[1])) {
return false;
}
$mac = hash_hmac('sha256', "quotometry.cart.v1:{$quote_id}:{$parts[1]}", $secret, true);
$expected = rtrim(strtr(base64_encode($mac), '+/', '-_'), '=');
return hash_equals($expected, $parts[2]) && (int) $parts[1] >= time();
}
import base64, hashlib, hmac, time
def handoff_ok(quote_id: str, signature: str, secret: str) -> bool:
parts = signature.split(".")
if len(parts) != 3 or parts[0] != "v1" or not parts[1].isdigit():
return False
mac = hmac.new(secret.encode(), f"quotometry.cart.v1:{quote_id}:{parts[1]}".encode(), hashlib.sha256).digest()
expected = base64.urlsafe_b64encode(mac).rstrip(b"=").decode()
return hmac.compare_digest(expected, parts[2]) and int(parts[1]) >= int(time.time())
When a signature does not verify, refuse the add and ask the buyer to price the part again. Never fall back to adding it unsigned.
Custom cart in Node.js
In the browser, post the quote id and its signature to your server:
// browser
document.querySelector('quotometry-quote').addEventListener('quotometry:cart-add', async (e) => {
await fetch('/cart/add', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ quoteId: e.detail.quote_id, signature: e.detail.signature }),
});
location.href = '/cart';
});
On the server, verify the signature, read the quote and add the line:
// server
app.post('/cart/add', async (req, res) => {
if (!handoffOk(req.body.quoteId, req.body.signature, process.env.QUOTOMETRY_SHARED_SECRET)) {
return res.status(400).json({ error: 'not this shop’s quote' });
}
const quote = await fetch(`https://api.quotometry.example/v1/quotes/${req.body.quoteId}`, {
headers: { authorization: `Bearer ${process.env.QUOTOMETRY_SECRET_KEY}` },
}).then((r) => r.json());
// Minor units and a currency code, never a float.
cart.add({
sku: `quotometry:${quote.number}`,
title: `Custom part ${quote.number}`,
amountMinor: quote.price.totals.total.amount_minor,
currency: quote.price.totals.total.currency,
metadata: { quotometry_quote_id: quote.id },
});
res.json({ ok: true });
});
At checkout, convert the quote into an order:
await fetch('https://api.quotometry.example/v1/orders', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.QUOTOMETRY_SECRET_KEY}`,
'content-type': 'application/json',
'idempotency-key': orderId, // your own order id: stable across retries
},
body: JSON.stringify({ quote_id: quoteId, customer, accepted_terms: true }),
});
customer needs at least email and name. A retry with the same Idempotency-Key returns the first order
(with created: false) instead of creating a second one.
Custom cart in PHP
$quote = json_decode(file_get_contents(
"https://api.quotometry.example/v1/quotes/{$quoteId}",
false,
stream_context_create(['http' => [
'header' => "Authorization: Bearer {$_ENV['QUOTOMETRY_SECRET_KEY']}\r\n",
]])
), true);
$minor = $quote['price']['totals']['total']['amount_minor'];
$cart->add("quotometry:{$quote['number']}", $minor, ['quotometry_quote_id' => $quote['id']]);
Custom cart in Python
import os, httpx
quote = httpx.get(
f"https://api.quotometry.example/v1/quotes/{quote_id}",
headers={"Authorization": f"Bearer {os.environ['QUOTOMETRY_SECRET_KEY']}"},
).raise_for_status().json()
cart.add(
sku=f"quotometry:{quote['number']}",
amount_minor=quote["price"]["totals"]["total"]["amount_minor"],
currency=quote["price"]["totals"]["total"]["currency"],
metadata={"quotometry_quote_id": quote["id"]},
)
WooCommerce
The WordPress plugin does all of this for you. The code below is for shops whose theme needs to own the flow.
Put the widget on a page with the shortcode or a Custom HTML block, then add the quote as a cart item at a price your server set:
add_action('wp_ajax_quotometry_add', 'quotometry_add');
add_action('wp_ajax_nopriv_quotometry_add', 'quotometry_add');
function quotometry_add() {
$quote_id = sanitize_text_field($_POST['quote_id'] ?? '');
$signature = sanitize_text_field($_POST['signature'] ?? '');
if (!quotometry_handoff_ok($quote_id, $signature, get_option('quotometry_shared_secret'))) {
wp_send_json(['ok' => false], 400);
}
$quote = quotometry_fetch_quote($quote_id); // GET /v1/quotes/{id} with your secret key
$item_key = WC()->cart->add_to_cart(
(int) get_option('quotometry_product_id'), // a hidden "Custom part" product
1,
0,
[],
['quotometry_quote_id' => $quote['id'], 'quotometry_minor' => $quote['price']['totals']['total']['amount_minor']]
);
wp_send_json(['ok' => (bool) $item_key]);
}
add_action('woocommerce_before_calculate_totals', function ($cart) {
foreach ($cart->get_cart() as $item) {
if (isset($item['quotometry_minor'])) {
// Assumes a currency with two decimal places.
$item['data']->set_price($item['quotometry_minor'] / 100);
}
}
});
Convert the quote into a Quotometry order from woocommerce_payment_complete, using the WooCommerce order id as
the idempotency key.
Shopify
Shopify does not let an app set an arbitrary line price on the storefront, so use a draft order:
quotometry:cart-addposts the quote id and its signature to your app.- Your app checks the signature, fetches the quote, and creates a Shopify draft order with a custom line item
at that price and
quotometry_quote_idin its attributes. It returns the draft’s invoice URL. - You send the buyer to that invoice to pay.
- On
orders/create, your app readsquotometry_quote_idfrom the order and converts the Quotometry quote into an order.
Whatever your cart is
Three things matter: the price comes from your server, the quote id travels with the line, and the signature is checked before either. Everything else is your cart’s own business.
Next steps
- Webhooks: hear about orders that the widget places itself.
- Customer tokens and B2B pricing: the shared secret and signed-in buyers.
- Errors:
price_changed,quote_expiredand the other order codes. - Embedding: the WordPress plugin and the Shopify app.