Developer docs menu

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_… or sk_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

  1. The buyer configures a part in the widget and presses your cart button.
  2. The widget fires quotometry:cart-add with { quote_id, number, totals, signature }.
  3. Your front end posts the quote_id and the signature to your own back end.
  4. 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.
  5. 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:

  1. quotometry:cart-add posts the quote id and its signature to your app.
  2. 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_id in its attributes. It returns the draft’s invoice URL.
  3. You send the buyer to that invoice to pay.
  4. On orders/create, your app reads quotometry_quote_id from 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