logo
  • Product
  • Solutions
  • Developers
  • Resources
  • Pricing
  • Marketplace
  • Log in
  • Create a store
  • Product

  • Pricing
  • Marketplace
  • Try for free
  • Log In
  • Merchandising

  • Operations

  • Building

  • Integrations

  • Products

    Powerful modeling and versatile presentation of your entire catalog.

  • Subscriptions

    Sell recurring physical and virtual products alongside one-time offerings.

  • Discounts

    Get the sale with coupons, BXGY promotions, and automatic discounts.

  • Wholesale

    Sell B2B like it's DTC, along with volume pricing, customer groups, and invoicing.

  • Content

    Manage all your products content through the admin dashboard.

  • Users

    Multi-store admin accounts and role-based permission controls.

  • Customers

    Manage customer info, generate reports, and see buyer activity.

  • Orders

    Edit orders anytime and get the right information for smooth fulfillment.

  • Fulfillment

    Ship from multiple locations, track inventory, and split shipments.

  • Reporting

    Monitor your store's performance to ensure you have visibility across the business.

  • Storefronts

    Swell storefronts are fully customizable, allowing you to create just the right experience.

  • Checkouts

    Use our hosted checkout, integrate with a partner, or build a custom flow.

  • Payments

    Connect multiple gateways simultaneously, store cards, and split payments.

  • Internationalization

    Go global with region-specific languages, pricing, and payment methods.

No-code integrations

Connect with 40+ services for marketing, payments, fulfillment, automation, and more.

See all integrations →

Use Cases

  • Direct-to-consumer

    Tell your story and give customers a unique shopping experience

  • Subscriptions

    Sell personalized subscription bundles, memberships, and one-time items together

  • B2B/B2C

    Support retail and wholesale customers from one catalog and dashboard

  • Marketplaces

    Create a B2B or B2C marketplace with multi-vendor carts and split payouts

Customer Stories

  • Spinn Coffee

    A coffee revolution sparked by a connected machine and marketplace

  • Smashing magazine

    Global tax and shipping for complex product bundles

  • Infinitas Learning

    Delievering leading educational experiences in Europe

All customer stories →

Documentation

  • Quickstart

  • Backend API reference

  • Frontend API reference

  • Guides

  • Core concepts

  • Storefronts

Community

  • GitHub

  • Discussion forum

  • Changelog

  • API status

Resources

  • Help Center

    The latest industry news, updates and info.

  • Customer stories

    Learn how our customers are making big changes.

  • Become a partner

    For agencies creating innovative commerce experiences.

Latest blog posts

  • deep-dive-app-functions
    Nov 06, 2025

    Build smarter workflows with App Functions

  • storefronts-v2-and-the-future-of-swell-apps
    Oct 22, 2025

    Storefronts V2 and the future of Swell Apps

  • Changelog

  • API Status

  • Contact us

Blog

How to Build a B2B Quote-to-Order Workflow in Headless Commerce

A build guide to the B2B quote-to-order state machine: nine quote states, a worked custom data model, approval chains, expiry, reprice-on-convert and the audit trail.

Swell Team | October 07, 2026

On October 1, 2025 the US federal micro-purchase threshold rose to $15,000. Below that line a government buyer swipes a card. Above it, the purchase becomes a requisition with an approval chain behind it, and somebody has to produce a quote. That threshold, defined in FAR 2.101, is why quote-to-order exists as a workflow at all.

Commercial procurement works the same way at different numbers, so the developer question is narrow and answerable: what does a quote-to-order flow consist of, in states, fields and endpoints? Roughly nine states, one snapshot rule that most first implementations get wrong, an approval record that stores an amount rather than a reference, and an append-only event log an accounts-payable analyst can read 14 months later.

The worked example below is a lab and MRO supply catalog selling reagents, consumables and calibration services to universities and research institutes, a vertical where quoting, punchout and tax exemption collide in a single order. Every mechanic maps onto real primitives: carts, orders, accounts, custom models and fields, account-group price rules and webhooks.

Key Takeaways

  • A quote lifecycle needs nine explicit states: draft, submitted, under review, repriced, pending approval, approved, expired, converted, rejected. Every transition names a trigger, an actor, a set of writes and an emitted event.
  • Quote line items must snapshot prices, not reference them. Storing `product_id` and resolving price live is the most common defect: an approver signs off on one total and the buyer converts at another, with nothing in the record showing the change.
  • Under UCC 2-205 a merchant's written firm offer is irrevocable for the stated period, and "in no event may such period of irrevocability exceed three months." That makes 90 days the practical ceiling on a validity window.
  • Approvals must store the amount approved. A `decided_amount` on each step is what lets an AP team prove the invoice matches what was authorized.
  • Plan retention for 7 years. The IRS lists 3, 6 and 7 year periods depending on circumstance, and 17 CFR 210.2-06 requires seven years for records relevant to an audit or review.
  • Punchout (cXML) and EDI (X12 850, 855, 810) sit in front of the storefront. Approvals happen in the buyer's procurement system; the seller authenticates a session, applies contract pricing and returns a well-formed cart.

Why a Quote Exists at All

Kent State University names Fisher Scientific and VWR International as vendors of choice for standard scientific supplies, ordered through punchout catalogs inside the university's own procurement tool. Florida State University moved Fisher Scientific from a hosted catalog to a punchout catalog. The University of Massachusetts goes further and tells buyers that most of its punchout suppliers can upload quotes directly into the punchout, naming Agilent, Fisher, Illumina, Qiagen, Sigma and VWR among them.

That last detail is the whole workflow in one sentence. A rep builds the quote on the seller's side; the buyer retrieves it inside a procurement session and turns it into a requisition, then a purchase order. Whatever the seller produces has to survive that handoff.

A cart is a mutable basket that reprices itself against the catalog every time it is read. A quote is the opposite: a priced, time-bounded, versioned commitment showing the same numbers on the day it is approved and the day it is redeemed. The law reinforces that. A merchant's quote carrying an assurance it will be held open is a firm offer, irrevocable for the stated period and capped at three months, so a quote with no expiry is unbounded in exactly the wrong direction. Expiry is a field.

The Quote Lifecycle as a State Machine

Nine states cover every real case in distribution selling. Write them as an enum on the record, never as a set of booleans and never as a free-text status a rep types.

  • `draft`: line items are still being assembled. Nothing is committed.
  • `submitted`: pricing has been requested. The snapshot is taken here.
  • `under_review`: a named rep owns it.
  • `repriced`: unit prices, quantities, freight or lead times changed. A new version exists.
  • `pending_approval`: a threshold was breached and an approval chain is open.
  • `approved`: every required step holds a decision of approve.
  • `rejected`: a blocking step declined.
  • `expired`: the validity window lapsed before conversion.
  • `converted`: an order exists carrying the quote id and version.

Transitions: trigger, actor, writes, event

Every transition is a write to `status` plus an append to an event log. None of them deletes or overwrites a prior version.

  • draft to submitted. Trigger: buyer submits a cart for pricing. Actor: any authenticated contact on the account. Writes: create the quote, copy cart lines into `items` as a frozen snapshot, stamp `cart_id`, `date_submitted`, `date_expires`. Emits `quote.created`.
  • submitted to under_review. Trigger: a rep claims it. Actor: a user with a sales role on the account. Writes: `owner_user_id`, `date_review_started`. Emits `quote.updated`.
  • under_review to repriced. Trigger: rep edits pricing. Actor: the owning rep. Writes: `items[].unit_price_quoted`, `adjustments[]`, `version` incremented, prior version pushed onto `revisions[]`. Emits `quote.repriced`.
  • repriced to pending_approval. Trigger: the recalculated total, discount or margin crosses a policy band. Actor: the system, not a human. Writes: an `approval` record with populated `steps[]` and `amount_at_request`. Emits `approval.created`.
  • pending_approval to approved. Trigger: all required steps decide approve. Writes: per step `decision`, `decided_by_user_id`, `decided_at`, `decided_amount`; on the quote `status` and `date_approved`. Emits `quote.approved`.
  • pending_approval to rejected. Trigger: any required step declines. Writes: step `decision` and `comment`, quote `date_rejected`. Emits `quote.rejected`.
  • approved to converted. Trigger: buyer redeems. Writes: create the order from the snapshot, set quote `order_id` and `date_converted`. Emits `order.created` and `quote.converted`.
  • any open state to expired. Trigger: `date_expires` passes. Actor: a scheduled sweep. Writes: `status`, `date_expired`. Emits `quote.expired`.
  • expired to submitted. Trigger: reinstatement. Writes: a new version with a fresh snapshot. The lapsed version is never mutated.

One Worked Data Model

Swell exposes a model editor and a backend API for defining new models and extending built-in ones. A definition carrying a `collection` attribute generates its own API endpoint; omitting `collection` makes the model abstract, useful for shared field groups. Field types include `short_text`, `long_text`, `boolean`, `currency`, `date`, `int`, `array`, `objectid` and `collection` for repeatable groups. The model customization guide has the exact envelope; the sketches below use those types.

The quote model

```json
{
"name": "Quote",
"collection": "quotes",
"fields": {
"number": { "type": "short_text", "required": true, "unique": true },
"version": { "type": "int", "default": 1 },
"status": { "type": "short_text", "default": "draft",
"enum": ["draft", "submitted", "under_review", "repriced",
"pending_approval", "approved", "rejected",
"expired", "converted"] },
"account_id": { "type": "objectid", "model": "accounts", "required": true },
"owner_user_id": { "type": "objectid", "model": "users" },
"cart_id": { "type": "objectid", "model": "carts" },
"order_id": { "type": "objectid", "model": "orders" },
"approval_id": { "type": "objectid", "model": "approvals" },
"currency": { "type": "short_text", "default": "USD" },
"reprice_policy": { "type": "short_text", "default": "revalidate",
"enum": ["honor", "revalidate", "requote"] },
"margin_floor_pct": { "type": "int", "default": 18 },
"grand_total": { "type": "currency" },
"date_submitted": { "type": "date" },
"date_approved": { "type": "date" },
"date_expires": { "type": "date", "required": true },
"date_converted": { "type": "date" },
"items": { "type": "collection", "fields": { } },
"revisions": { "type": "array" }
}
}
```

Why the line items must be a snapshot

This is the bug every first implementation ships. A naive `items` collection stores `product_id` and `quantity`, and the quote view resolves price live. The quote then shows one total on Tuesday and a different one on Friday. Worse, an approver signs off on $48,200 and the buyer converts at $51,900 with no record that the amount moved.

Freeze the line at submission, and freeze more than the price, because reprice-on-convert and margin checks both depend on what cost was when the commitment was made.

```json
"items": {
"type": "collection",
"fields": {
"product_id": { "type": "objectid", "model": "products", "required": true },
"variant_id": { "type": "objectid" },
"sku_snapshot": { "type": "short_text", "required": true },
"name_snapshot": { "type": "short_text" },
"quantity": { "type": "int", "required": true },
"unit_price_list": { "type": "currency", "required": true },
"unit_price_quoted": { "type": "currency", "required": true },
"unit_cost_snapshot": { "type": "currency" },
"price_rule_source": { "type": "short_text" },
"tax_code_snapshot": { "type": "short_text" },
"lead_time_days": { "type": "int" },
"hazard_class": { "type": "short_text" },
"requires_review": { "type": "boolean", "default": false }
}
}
```

`unit_cost_snapshot` makes reprice-on-convert possible at all. `price_rule_source` records which pricing rule produced `unit_price_list`, turning a pricing dispute into a lookup. `hazard_class` and `requires_review` exist because a lab catalog contains lines that must never convert unattended.

The approval model

```json
{
"name": "Approval",
"collection": "approvals",
"fields": {
"quote_id": { "type": "objectid", "model": "quotes", "required": true },
"policy_id": { "type": "objectid", "model": "approval_policies" },
"status": { "type": "short_text", "default": "pending",
"enum": ["pending", "approved", "rejected", "cancelled"] },
"amount_at_request": { "type": "currency", "required": true },
"steps": {
"type": "collection",
"fields": {
"sequence": { "type": "int", "required": true },
"role": { "type": "short_text" },
"approver_user_id": { "type": "objectid", "model": "users" },
"threshold_min": { "type": "currency" },
"threshold_max": { "type": "currency" },
"required": { "type": "boolean", "default": true },
"decision": { "type": "short_text",
"enum": ["pending", "approve", "reject"] },
"decided_by_user_id": { "type": "objectid", "model": "users" },
"decided_at": { "type": "date" },
"decided_amount": { "type": "currency" },
"delegated_from_user_id": { "type": "objectid", "model": "users" },
"comment": { "type": "long_text" }
}
}
}
}
```

The load-bearing field is `decided_amount`. An approval is an approval of a number, not of a record. Store the number on the step at decision time and a later reprice cannot quietly inherit an old authorization.

Custom fields on account, cart and order

Swell's account model already carries `type` (individual or business), `group` (the customer group used for special price rules), `balance` for store credits, `vat_number` and an `attributes` object. Trade credit and terms are different concepts and need their own fields.

  • On account: `credit_limit` (currency), `credit_exposure` (currency), `payment_terms_net_days` (int), `requires_po` (boolean), `approval_policy_id` (objectid), `tax_exemption_id` (objectid).
  • On cart: `quote_id`, `quote_version`, `requested_delivery_date`, `punchout_session_id`. The cart model already supplies `account_id`, `items`, `sub_total`, `grand_total` and a `status` of active, converted, abandoned or recovered.
  • On order: `quote_id`, `quote_version`, `approval_id`, `po_number`, `reprice_delta`. The order model also has a `metadata` object for arbitrary data set in a checkout flow, the fastest place to park a PO number until AP starts querying by it.

Price Resolution at Cart Time

On Swell, a product's prices array holds rules, each with a `price` plus optional `account_group`, `quantity_min` and `quantity_max`. When the buyer's group and the line quantity both match, that rule overrides the product's `price` and `sale_price`. `purchase_options` carries its own prices array for standard versus subscription buying and can be restricted by `account_groups`. Resolution is therefore: read the account's `group`, read the line quantity, filter matching rules, pick one, and stamp the winner into `price_rule_source`.

Precedence when two rules match

Two rules matching one line is normal, and an undefined tiebreak is a support ticket waiting to happen. Define the order explicitly:

  • Group plus quantity band wins. Most specific condition set first.
  • Group only beats a quantity band with no group. A negotiated relationship outranks a volume break.
  • Quantity band only beats base price.
  • On a tie inside one tier, prefer the narrowest band, then the lowest price. Do not leave it to array insertion order.

One division keeps this sane. Catalog price rules serve the standing relationship, so a group that always buys nitrile gloves at 22% off list is a rule. A deal-specific number that must not follow the catalog belongs on the quote snapshot. Mixing them produces a catalog nobody dares edit.

Minimum order quantity and order multiples

The product model carries `quantity_min`, the minimum sellable quantity, and `quantity_inc`, a multiple the product must be sold in. Amber bottles cased twelve to a carton are `quantity_inc: 12`. Enforce both at add-to-cart, at quote submit, and at convert, the same three gates bulk ordering workflows depend on.

Approval Chains, Thresholds and Delegation

Hard-coded threshold logic becomes unmaintainable at the third exception. Model the policy as a record and evaluate it at reprice time:

```json
{
"name": "Approval policy",
"collection": "approval_policies",
"fields": {
"account_group": { "type": "short_text" },
"active": { "type": "boolean", "default": true },
"rules": {
"type": "collection",
"fields": {
"sequence": { "type": "int" },
"amount_min": { "type": "currency" },
"amount_max": { "type": "currency" },
"discount_pct_min": { "type": "int" },
"margin_pct_max": { "type": "int" },
"category_code": { "type": "short_text" },
"approver_role": { "type": "short_text", "required": true },
"required": { "type": "boolean", "default": true }
}
}
}
}
```

Route on more than order value. The signals that matter are quote total, discount percentage against list, gross margin computed from `unit_cost_snapshot`, and category, because a regulated or hazardous category routes to a compliance role regardless of amount. Ship-to site becomes a fifth once an account has facilities with different budget owners. Three bands is enough to start: rep, manager, finance.

Multi-step chains and delegation

A chain is an ordered `steps[]` array. Steps sharing a `sequence` evaluate in parallel; a higher sequence waits. `required: false` marks an informational reviewer who does not block.

Delegation is where audit trails rot. The tempting implementation swaps `approver_user_id` to the delegate, erasing the fact that someone else was accountable. Record it instead: leave `approver_user_id` alone, set `decided_by_user_id` to whoever clicked, and set `delegated_from_user_id`. The record then answers both questions.

Notifications by webhook

Swell webhooks are configured from the dashboard or by a Swell App and receive an HTTP POST whose JSON body carries the event id, creation date, model, event type and data. Events follow a `model.action` shape such as `order.created`. For a custom model, create the webhook through the API referencing the events listed on that model.

One operational detail deserves an alert. A Swell webhook records `attempts_failed`, `date_last_success` and `auto_disabled`, and is automatically disabled seven days after a first failure with no subsequent success. In an approval system a silently disabled webhook means notifications stop while quotes keep aging out. Monitor `attempts_failed`, not the inbox.

Expiry and Reprice on Convert

`date_expires` is stamped at submission from a policy window: 14, 30 or 60 days for consumables, longer for capital equipment. Ninety days is the practical outer bound given the three-month statutory cap on firm offers. Two implementation notes. Do not compute expiry only at read time, or a report querying stored status will disagree with the storefront; run a scheduled sweep that writes `status` and `date_expired`. And emit `quote.expired`, because a lapse is a sales trigger as much as an accounting one.

When an approved quote is redeemed after a cost change

Here is the case nobody designs for until it bites. A quote for 400 liters of HPLC-grade acetonitrile is approved at $48,200 on day 3. The buyer's requisition clears on day 27. In between, landed cost rose 9% and quoted margin now sits below the floor. What converts? Make the answer a stored policy so it is a business decision rather than a code path:

  • `honor`: convert at quoted prices regardless of cost movement. Simple, defensible with buyers, costs margin. Right for short windows and strategic accounts.
  • `revalidate`: recompute current cost, compare against `unit_cost_snapshot`, and if margin falls under `margin_floor_pct`, return the quote to `pending_approval` with a new approval whose `amount_at_request` reflects the new number. The right default for most catalogs.
  • `requote`: expire and issue a new version. Cleanest legally, worst for conversion rate, correct for volatile commodity inputs.

Whichever fires, write `reprice_delta` plus before and after totals onto the resulting order. The conversation with accounts payable will happen, and the useful version involves a field rather than an email thread. This is also where UCC 2-207 becomes relevant, since terms in an acknowledgment that differ from the offer do not automatically become part of the contract.

Converting a Quote Into an Order

Conversion is one logical transaction with an ordering that matters. Create the order from the snapshot with explicit unit prices so the platform does not reprice from the live catalog. Set `quote_id`, `quote_version`, `approval_id` and `po_number` on the order. Then set the quote to `converted` and write `order_id`.

If the order create succeeds and the quote update fails, the quote stays redeemable and the account can order twice. Guard the sequence with an idempotency key derived from quote id plus version, the pattern Stripe documents for idempotent requests, so a replay returns the original order. Swell order statuses are `pending`, `draft`, `payment_pending`, `delivery_pending`, `hold`, `complete` and `canceled`; a converted quote usually lands on `draft` when a rep confirms stock and freight, or `payment_pending` on terms.

Purchase order numbers and net terms as order state

Terms belong to the converted order and its invoice, not to the quote. Swell's invoice model carries `net_days` (days the invoice stays open before it is past due), `date_due`, `payment_due`, `pastdue`, `paid` and a `status` of pending, paid, unpaid or void. The order carries `billing.method`, which can be card, account, amazon, paypal or any manual method defined in payment settings, plus `payment_balance`, `payment_total` and `paid`.

Minimum wiring: copy `payment_terms_net_days` from the account onto the order at conversion, set `po_number`, issue the invoice with `net_days` and `date_due`, and subscribe to invoice events so AR or an ERP picks up the issue. The full accounts-receivable treatment, including credit-line underwriting, aging ladders and reconciliation, is covered in Swell's B2B subscription and recurring billing guide.

Credit limits

Check available credit when the approval chain opens, at conversion, and at invoice issue. Available credit is `credit_limit` minus `credit_exposure` minus open approved quotes, and that third term is the one implementations forget. It is how an account with a $100,000 limit ends up holding $260,000 of approved, unconverted quotes. Note also that Swell's account `balance` is store credit, a different instrument from a trade credit line.

The Audit Trail Accounts Payable Will Ask For

Swell writes an event record for activity across standard models including accounts, carts, invoices, orders, payments, products, promotions, shipments and subscriptions. Each carries `model`, `type`, `data`, `user_id`, `req_id`, `webhooks_pending` and a `webhooks` array. `req_id` is quietly the most useful field in a dispute, because it ties a repricing action to the exact API request that performed it. Custom models can be configured with their own events.

Add a dedicated append-only model for the fields a generic payload will not carry:

```json
{
"name": "Quote event",
"collection": "quote_events",
"fields": {
"quote_id": { "type": "objectid", "model": "quotes", "required": true },
"quote_version": { "type": "int", "required": true },
"from_status": { "type": "short_text" },
"to_status": { "type": "short_text", "required": true },
"actor_user_id": { "type": "objectid", "model": "users" },
"actor_role": { "type": "short_text" },
"amount_before": { "type": "currency" },
"amount_after": { "type": "currency" },
"reason": { "type": "long_text" },
"request_id": { "type": "short_text" },
"source": { "type": "short_text",
"enum": ["storefront", "admin", "api", "punchout", "edi", "system"] }
}
}
```

Never update a row in that collection. Corrections are new rows. The `source` enum earns its place the first time somebody asks whether a price change came from a rep in the admin or from an automated feed.

Reconstructing a purchase order's provenance

The question an AP analyst asks 14 months later is always a version of: who approved this, at what amount, and is that the amount invoiced? A well-built model answers in four hops. Order to `quote_id` and `quote_version`. Version to the matching entry in `revisions[]`. Quote to `approval_id`. Approval steps to `decided_by_user_id`, `decided_at` and `decided_amount`. If `decided_amount` was never stored the chain breaks at the last hop, and the honest answer becomes "someone approved something."

Retention

Set retention from the outside in. The IRS lists 3 years as standard, 6 where unreported income exceeds 25% of gross income, 7 for a worthless securities or bad debt claim, and indefinitely where no return was filed. Under 17 CFR 210.2-06 the accountant who audits or reviews an issuer's financial statements must retain records relevant to that audit or review for seven years, which in practice sets the horizon for the seller-side records an auditor will pull. Quotes, approvals and the event log are financial records, so size storage on a seven-year horizon.

Punchout and EDI: The Escape Hatch in Front of the Storefront

Punchout is a session handoff, not a checkout. The buyer's procurement system posts a cXML `PunchOutSetupRequest`, the supplier returns a start URL, the buyer shops in the supplier's interface, and the supplier posts back a `PunchOutOrderMessage` containing the cart. The procurement system turns that into a requisition and, after its own approvals, a purchase order. SAP's guide to developing a punchout site describes identification from the credential domain and network id, and how `BuyerCookie` distinguishes concurrent sessions. Coupa documents an equivalent supplier integration. The specification lives at cxml.org, with the current cXML Reference Guide at version 1.2.071, dated August 14, 2026.

The architectural consequence matters: approvals in a punchout relationship happen in Ariba, Coupa or Jaggaer, not in the seller's system. The seller's obligations shrink to three. Authenticate the session and resolve it to an account, apply that account's contract pricing, return a well-formed cart. A quote a rep already built must be retrievable inside that session, which is exactly what UMass instructs its buyers to request.

EDI is the same idea, asynchronously

For accounts that will not punch out, X12 EDI covers the same ground in batch. An 850 purchase order arrives, an 855 acknowledgment goes back with accept, reject or change, an 810 invoice follows, and a 997 functional acknowledgment confirms syntactic receipt at each hop. X12 maintains the transaction set catalog, where 810 is defined around billing for goods and services provided and 997 around control structures for acknowledgments. Sequencing matters: an 850 arrives after the buyer's approvals are finished, so it maps to an order create, not to a quote request. Quoting in an EDI relationship happens by email or through the punchout catalog.

Neither punchout nor EDI is configuration. Both are integration projects with per-trading-partner mapping, a test cycle against the buyer's procurement team, onboarding queues on their side and ongoing spec drift. Treat each large institutional account as its own project, and build the state machine first so there is something worth connecting to.

Reality Check: Configuration Versus Custom Work

  • Configuration on a modern platform: account groups and membership; group-scoped price rules with quantity bands; `quantity_min` and `quantity_inc`; custom fields on accounts, carts, orders and products; new custom models with generated API endpoints; webhook subscriptions; invoices with `net_days` and `date_due`.
  • Genuinely custom everywhere, Swell included: the quote object and its state machine, including versioning and the snapshot rule; the approval policy engine, threshold routing and delegation; the expiry sweep and reprice-on-convert decision; the buyer-facing quote interface and the rep-facing repricing interface; punchout and EDI endpoints; the immutable event log, where built-in events lack fields an auditor needs.

Swell does not ship a native RFQ or quote management module, and a build guide implying otherwise would be wrong. What it ships is the modeling surface underneath one: custom models that generate their own API endpoints, custom fields on every built-in model, account groups with quantity-banded price rules, subscriptions and recurring billing in the same API as carts and orders, an event and webhook system with request-level attribution, and a published skills package for AI coding agents documenting the backend, storefront and app surfaces. For how that compares against suites that do ship RFQ modules, the B2B and wholesale platform roundup covers the field directly.

Tax exemption certificates

Research institutions, hospitals and government labs are frequently exempt, so the tax engine cannot be wired last. All 24 Streamlined member states accept the Streamlined Sales Tax exemption certificate, and a seller that accepts a fully completed certificate in good faith is relieved of liability for the tax, though not every member state allows every exemption reason printed on the form; the certificate instructions define completeness. Model it like a quote, because it expires like one: a `tax_exemptions` collection with `account_id`, `jurisdiction`, `certificate_number`, `reason_code`, `date_issued`, `date_expires` and a file reference, swept on the same schedule and resolved at both submission and conversion.

Lines that must never convert unattended

A lab catalog contains controlled substances, scheduled chemicals, dry ice shipments and items requiring a hazardous materials declaration. Set `requires_review` on the snapshot line, block the approved-to-converted transition while any line carries it, and route those quotes to a compliance role by `category_code`. The cost of getting this wrong is regulatory, not commercial.

Platform cost

A quote system is API-heavy: policy evaluation, expiry sweeps, punchout session resolution and approval checks all generate requests. Swell's published plans are revenue-tiered and billed yearly: Starter at $29 per month with a $50K trailing-twelve-month revenue ceiling and 2% beyond it, Basic at $79 with a $250K ceiling and 1.5%, Standard at $299 with a $1M ceiling and 1%, and Unlimited at $2,250 with a $5M ceiling and 0.4%, with custom pricing above $10M a year. API requests beyond the included volume cost $5 per 100,000 requests or part thereof, and extra storage is $5 per gigabyte per month. An approval poller hitting the API every 30 seconds for every open quote shows up on that line, so use webhooks and scheduled sweeps rather than polling.

Choosing the Right Depth

Build the full state machine if negotiated pricing covers a meaningful share of revenue, reps reprice regularly, buyers have internal approval chains, and accounts payable has asked for PO provenance more than once. Below roughly a dozen quotes a week it is overkill, and a thinner version wins: a draft order, an expiry date and a single approver, with the snapshot rule still enforced, because that part is not optional at any scale.

Invest in punchout or EDI first if your largest accounts are universities, health systems, national labs or government agencies. For those buyers the procurement system is the buying experience, and a quote portal they never open is worth less than a working cXML endpoint. The signal: prospects ask which procurement networks you are on before they ask about your catalog.

Choose Swell if you intend to own this workflow. The argument is about mechanics rather than positioning: custom models become first-class API collections with generated endpoints, so a `quotes` collection is queryable the way `orders` is; custom fields attach to built-in models without forking anything; account groups and quantity-banded price rules resolve inside the product's own `prices` array; subscriptions live in the same API, which matters when standing reagent orders and capital quotes share an account; and events carry `req_id`, the request-level attribution an audit trail rests on.

Choose an established B2B suite instead if you need RFQ, punchout and EDI working on day one and will accept the suite's data model and its opinions about how a quote behaves. That is a legitimate trade. OroCommerce and BigCommerce B2B Edition, for instance, ship RFQ and punchout capabilities Swell leaves to you.

Frequently Asked Questions

Should a quote be a custom model or a draft order?

A custom model, in almost every case. A draft order inherits semantics that fight the lifecycle: it prices itself live, it appears in order reports, and it has no natural home for versions, revisions or approval state. Use a draft order only for the thinnest implementation, where expiry and one approver are the whole requirement.

Why must quote line items store prices instead of referencing the catalog?

Because a quote is a commitment and a catalog is not. If the line resolves price live, the total shown to an approver can differ from the total charged at conversion, with no record of the change. Snapshotting `unit_price_list`, `unit_price_quoted` and `unit_cost_snapshot` at submission makes the quote stable, makes margin checks possible at conversion, and makes the approval defensible.

How long should a quote stay valid?

Fourteen to sixty days covers most consumable and MRO catalogs, with longer windows for capital equipment where the buyer's approval cycle is slow. Ninety days is a sensible outer limit, since UCC 2-205 caps a merchant's written firm offer at three months. Store the window as `date_expires` and sweep it on a schedule rather than computing it at read time.

Where does the purchase order number live?

On the converted order, not on the quote. A PO number is issued by the buyer's procurement system after their approvals complete, which happens after the quote is approved on the seller's side. Park it in the order `metadata` object for a first implementation and promote it to a dedicated custom field once accounts payable starts querying by it.

Do approvals belong in the storefront or in the buyer's procurement system?

Both, for different buyers. Accounts on Ariba, Coupa or Jaggaer approve inside their own system, and duplicating that chain creates two sources of truth. Most mid-market buyers have no procurement platform and need the chain in the seller's system. Build the seller-side chain and make it skippable for punchout sessions.

What breaks first in a homegrown quote workflow?

Three things, in this order. Live-priced line items, which produce quotes that change value between approval and conversion. Missing `decided_amount` on approval steps, which makes provenance unreconstructable. And a silently disabled webhook, which stops approval notifications while quotes keep expiring. All three are cheap to prevent at design time and expensive to retrofit.

Does Swell provide a native quote or RFQ module?

No. Swell provides the primitives a quote workflow is built from: custom models with generated API endpoints, custom fields on built-in models, account groups with quantity-banded price rules, invoices with net terms, and an event and webhook system. The quote object, its state machine and the approval engine are custom work on Swell, as on most platforms without a dedicated B2B module.

Next-level commerce for everyone.

X.comGitHubLinkedIn

Subscribe to our newsletter for product updates and stories

Subscribe

Resources

Help CenterDeveloper CenterCommunityAgenciesChangelogLearn

Use cases

SubscriptionsB2B WholesaleMarketplaceOmnichannelDirect-to-consumerEnterprise

Explore

FeaturesPricingIntegrationsCustomer stories

Developers

OverviewDocumentationGuidesStorefrontsHeadlessSwell Apps

Company

BlogAbout usPartnersContact us

© 2026 Swell. All rights reserved.

Privacy PolicyTerms of Service