All field notes
B2B Commerce

B2B Checkout: Net Terms, PO Numbers and Approval Chains

The retail checkout assumes one person with a card. Trade buying assumes a budget holder, a credit limit and somebody who has to sign it off.

360 Expert Solutions3 min read

A B2B checkout is not a retail checkout with an invoice option bolted on. It has to carry a purchase order number, respect a credit limit the ERP owns, and route orders above a threshold to an approver — without becoming the slowest part of the buying process.

Retail checkout optimisation is a well-trodden field: remove fields, remove steps, remove friction. Applying that instinct directly to B2B produces a checkout that finance refuses to sign off, because in trade buying some of the friction is the control.

A wholesale order frequently involves three roles: the person who selects the goods, the person whose budget pays for them, and the finance function that decides whether this account can take on more exposure. A checkout that ignores those roles pushes the work into email, and the "digital" order ends up processed by hand anyway.

The four capabilities

  1. Purchase order capture

    A PO number field, validated to the customer’s format, carried through to the ERP and printed on the invoice. Often mandatory per account, not globally.

  2. Payment terms

    Net 30 / 60 / custom, offered only to accounts entitled to them, with everyone else falling back to card or bank transfer.

  3. Credit limit enforcement

    The available credit figure lives in the ERP. The checkout must read it at the moment of purchase, not trust a nightly copy.

  4. Approval routing

    Orders over a threshold become a pending request routed to a named approver, rather than an order.

The order lifecycle, with approvals in it

Order state flow for a B2B checkout with approvals. A buyer builds a cart, then the checkout validates the purchase order number format and calls the ERP for a live credit check. If credit is insufficient the order is blocked with a message and finance is notified. If the order value is under the account threshold it is placed directly. If it is over, it becomes a pending approval routed to a named approver, who can approve, reject or amend. Approved orders are placed and pushed to the ERP as a sales order with the PO number attached.

The approval branch is the part teams forget to design, and the part buyers judge you on.

Credit limits must be checked live

This is the single most common shortcut, and it is the one that costs real money. Teams sync an available-credit figure overnight because a live ERP call at checkout feels risky. Then a customer places four orders in one morning, each individually within the stale limit, and collectively far beyond it.

Check at the moment of purchase. Give the call a tight timeout and an explicit, deliberate fallback — and make the fallback a business decision rather than an accident of implementation.

javascript
const CREDIT_TIMEOUT_MS = 1200;

async function resolveCreditDecision({ accountId, orderTotal }) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), CREDIT_TIMEOUT_MS);

  try {
    const res = await fetch(`${ERP_BASE}/accounts/${accountId}/credit`, {
      signal: controller.signal,
      headers: { Authorization: `Bearer ${erpToken()}` }
    });
    if (!res.ok) throw new Error(`ERP ${res.status}`);

    const { availableCredit, termsCode } = await res.json();
    return {
      decision: orderTotal <= availableCredit ? 'allow' : 'block',
      availableCredit,
      termsCode,
      source: 'live'
    };
  } catch (err) {
    // Deliberate policy, agreed with finance and written down:
    // fall back to the last known figure, but only below a stated ceiling.
    const cached = await lastKnownCredit(accountId);
    const withinFallbackCeiling = orderTotal <= Math.min(cached, FALLBACK_CEILING);

    reportDegradedCreditCheck({ accountId, reason: err.message });

    return {
      decision: withinFallbackCeiling ? 'allow' : 'review',
      availableCredit: cached,
      source: 'cached',
      degraded: true
    };
  } finally {
    clearTimeout(timer);
  }
}
A live credit check with a bounded timeout and an intentional failure policy.

Enforcing rules inside the checkout

On Shopify Plus, the mechanisms are Checkout UI extensions for fields such as the PO number, and Shopify Functions for rules that must be enforced server-side — minimum order values, blocking a payment method the account is not entitled to, hiding delivery options that do not apply to a trade account.

The important distinction is that a UI extension is a presentation concern and a Function is an enforcement concern. Anything a determined buyer must not be able to bypass belongs in a Function. Validation that only exists in the UI is advice, not a control.

rust
#[shopify_function]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
    let minimum = input
        .cart
        .buyer_identity
        .as_ref()
        .and_then(|b| b.purchasing_company.as_ref())
        .and_then(|c| c.company.minimum_order.as_ref())
        .and_then(|m| m.value.parse::<f64>().ok())
        .unwrap_or(0.0);

    let subtotal = input.cart.cost.subtotal_amount.amount;

    let errors = if subtotal < minimum {
        vec![output::FunctionError {
            localized_message: format!(
                "Orders on this account start at {:.2}. Add {:.2} to continue.",
                minimum,
                minimum - subtotal
            ),
            target: "cart".to_owned(),
        }]
    } else {
        vec![]
    };

    Ok(output::FunctionRunResult { errors })
}
A Shopify Function enforcing a per-account minimum order value server-side.

Approvals without the delay

An approval step adds latency to the buying process, and buyers notice. Three things keep it tolerable: notify the approver immediately through a channel they actually read, let them approve without logging into a portal they have never used, and show the requester exactly where the order is sitting and who has it.

Also allow the approver to amend rather than only approve or reject. In practice a great many approvals are "yes, but drop the second line", and forcing a reject-and-rebuild cycle for that is how a well-intentioned control becomes the reason a customer starts phoning orders through again.

Key takeaways

  • B2B friction is not always waste — some of it is the financial control the account requires.
  • Check credit live at checkout. Nightly figures let several same-day orders each pass a stale limit.
  • Decide and document the ERP-unreachable failure mode before launch, and monitor the degraded path.
  • UI validation is advice; enforcement belongs server-side, in a Function or its equivalent.
  • Let approvers amend orders, not just approve or reject — it is what most approvals actually are.
B2BCheckoutNet TermsApprovalsShopify FunctionsCredit

360 Expert Solutions

E-commerce & ERP Integration Team

We build and scale B2B and DTC commerce on Shopify Plus, BigCommerce and WooCommerce, wired into NetSuite and Oracle. These articles are written by the engineers and architects who deliver those projects.

Building something like this?

We architect B2B commerce and ERP integrations end to end.

B2B Commerce

PunchOut Catalogues: Selling Into Enterprise Procurement

Large buyers do not shop on your website. They shop inside Ariba, Coupa or SAP, and they expect your catalogue to appear there. PunchOut is the protocol that makes your storefront a guest inside their procurement system — and it is usually a contractual precondition, not a nice-to-have.

Read
B2B Commerce

Customer-Specific Pricing That Survives 40,000 SKUs

The instinct is to precompute every customer-product price. With 800 accounts and 40,000 SKUs that is 32 million rows that go stale the moment a contract changes. The alternative is to store rules and resolve them — with caching designed around how buyers actually browse.

Read
ERP Integration

Real-Time Inventory Between NetSuite and Shopify Plus B2B

Most NetSuite-to-Shopify integrations poll on a timer. For B2B that is too slow: a single wholesale order can move a thousand units, and the next buyer sees stock that no longer exists. The fix is event-driven sync with a reconciliation pass behind it.

Read
Book