Developer docs menu

JavaScript SDK

Build your own quoting interface on the buyer API with @quotometry/api-client, the same client the widget uses: upload, price, quote and order.

This guide shows how to drive the buyer API yourself with @quotometry/api-client, the same client the widget uses, without its interface. Use it when the quoting flow belongs inside your own configurator, or when your design system is not negotiable. By the end you can upload a part, price it, save a quote and place an order.

Before you begin

  • A widget key, with your site’s origin in its Allowed origins. The SDK calls the buyer API from the browser, so the same origin check applies.
  • A build that can install npm packages. The client is not published to the public npm registry yet; get it from your Quotometry contact or from the repository.

Install and create a client

npm install @quotometry/api-client
import { QuotometryClient, formatMoney } from '@quotometry/api-client';

const quotometry = new QuotometryClient('https://api.quotometry.example', 'wgt_live_YOURKEY');

Every response is checked against a schema before you see it, so a field that changed shape fails loudly at the boundary instead of quietly three functions later. Every failure is an ApiError with status, code, issues and retryable. The client retries transient failures itself, including a 429 after its Retry-After.

1. Read what the shop offers

const boot = await quotometry.bootstrap();
// boot.configurations[] - each with its buyer option schema, currency and build volume
// boot.limits          - max_files, max_file_bytes, formats

Call bootstrap() once per page. It tells you which materials, colours and layer heights exist, and what control each option should be (cards, swatches, a slider, a stepper). Build your interface by rendering that schema, not a list you hard-coded: the shop changes it without telling you.

2. Upload a part

const modelId = await quotometry.upload(file, 'mm', (sent, total) => {
  progress.value = sent / total;
});

One call does the whole job: register, upload, complete.

  • Small files go in a single PUT to a presigned URL. Above the server’s threshold the client switches to a multipart upload, retries a failed part, and after a dropped connection asks the server what arrived and sends only the gap. A 100 MB part on mobile data does not start over because a tunnel interrupted it.
  • On a network that cannot reach the storage host at all (some providers and company firewalls block it), the client notices within six seconds and sends the same bytes through the API’s upload relay (PUT /v1/uploads/{id}/content or /parts/{number}). quotometry.model(id) does the same for the preview mesh and thumbnail: on such a network their URLs point at /v1/models/{id}/preview and /thumbnail on the API.
  • The file is hashed in the browser and the server checks the bytes that arrive, so a truncated upload is refused rather than quoted.

The unit argument matters for STL, which carries no units. Pass what the buyer told you (mm, cm, m or inch), or omit it and let Quotometry infer the unit from the part’s size.

3. Wait for the analysis

const model = await quotometry.waitForModel(modelId, (status) => setStatus(status));
// model.parts[] - id, name, dimensions_mm, volume_cm3, area_cm2, verdict, triangles

waitForModel streams the status over server-sent events and falls back to polling where EventSource is not available. Each part in the model can be priced on its own: an STL is usually one part, while a 3MF, AMF or glTF file often holds several.

4. Price it

const price = await quotometry.price(
  [{
    line_ref: 'a',
    model_part_id: model.parts[0].id,
    configuration_id: boot.configurations[0].id,
    selections: { material: 'petg', color: 'black', layer_height: 0.2, infill: 20 },
    quantity: 10,
  }],
  'DE',       // destination country, ISO 3166-1 alpha-2
  sessionId,
  { coupon: 'WELCOME10' },
);

formatMoney(price.totals.total); // "€184.50"

Call price() whenever a selection changes, debounced. Nothing is stored and the buyer is committed to nothing.

5. Save a quote, then order

const quote = await quotometry.createQuote(lines, 'DE', { customer_token });
const order = await quotometry.createOrder(
  quote.id,
  { email, name, company },
  { po_number, notes, metadata: { cart_id } },
  crypto.randomUUID(), // idempotency key
);

The last argument of createOrder is an idempotency key. If you retry with the same key, you get the same order back (with created: false) rather than a second order. Generate the key once, before the first attempt: a key generated inside a retry loop is not an idempotency key.

To give the buyer a link back to a saved quote, call quotometry.shareQuote(quote.id, email). Omit email to get the link without sending it.

When a part cannot be priced

const rfq = await quotometry.createRfq(lines, { email, name }, 'It has to fit a 6 mm shaft.', 'DE');

A request for quote is the path for an oversize part, a DFM problem the shop blocks on, or a quantity past the shop’s review threshold. It lands in the shop’s Inbox and a person answers it.

Methods

MethodReturnsDescription
bootstrap()BootstrapThe widget’s configurations, options, limits, theme and checkout settings.
upload(file, unit?, onProgress?)string (model id)Uploads a file and starts its analysis.
model(id)ModelViewThe model’s status and parts, once.
waitForModel(id, onStatus)ModelViewWaits until the analysis finishes.
price(lines, country?, sessionId?, extras?)PriceViewPrices line items. extras takes coupon, customer_token, customer_email, region, postcode and custom_fields.
createQuote(lines, country?, extras?)QuoteViewSaves a quote.
shareQuote(quoteId, email?)ShareViewA link back to the quote, optionally e-mailed to the buyer.
createOrder(quoteId, customer, extras, idempotencyKey)OrderViewPlaces an order from a quote.
createRfq(lines, customer, message, country?, customFields?){ id, number, status, message }Sends a request for quote.

Types and helpers

Every type is exported and inferred from the schemas: Bootstrap, BootstrapConfig, OptionSpec, Selections, ModelView, Part, PriceView, LineView, Totals, QuoteView, ShareView, OrderView, LineItemRequest, Customer, PriceExtras, Money and ApiError.

  • formatMoney(value, locale) renders a Money value the way its currency and the locale expect.
  • t(text, locale) picks a string from a translated label, falling back to English.

Next steps