← All posts NetSuite & ERP 12 min read

B2B WooCommerce Customer Portal with NetSuite: Syncing Invoices, Credit Limits, and Order History

How to build a B2B WooCommerce customer portal that syncs invoices, credit limits, account-specific pricing, and full order history from NetSuite in real time.

B2B WooCommerce customer portal architecture with NetSuite invoices, credit, and orders
Quick Summary

B2B WooCommerce Customer Portal with NetSuite — Syncing Invoices, Credit Limits, and Order History

  • A B2B customer portal in WooCommerce lets wholesale buyers view open invoices, credit balance, and order history — data that lives in NetSuite, not WooCommerce.
  • Three of the portal’s four NetSuite calls are reads. As of 2026, they collapse into a single SuiteQL query instead of three separate RESTlet endpoints — the original design most teams still ship.
  • NetSuite already blocks over-limit orders on its own with an automatic credit hold. The portal still needs its own checkout-time check, because the hold only fires once the order reaches NetSuite — after WooCommerce has already told the buyer checkout succeeded.
  • Invoice payment write-back uses the customer payment record’s apply sublist — a documented REST JSON shape, not a custom SuiteScript endpoint.
5,000 units
RESTlet governance budget per request — the ceiling a three-endpoint portal read pays even when one call could do the job
5 concurrent
Standard-tier NetSuite concurrency pool — shared by every portal page load, checkout call, and background sync job at once
A single SuiteQL POST returns invoices, credit, and order history together — no RESTlet required for the read path
Customer Center
NetSuite’s own free self-service portal — the build-vs-buy comparison this guide makes explicit

A self-service B2B portal in WooCommerce is one of the highest-value use cases for NetSuite integration. Wholesale buyers log in, see their account standing, view open invoices, pay outstanding balances, and place new orders — all without emailing or calling a sales rep. The technical challenge is that the financial data (invoices, credit, payment history) lives in NetSuite, while the UI and payment processing happen in WooCommerce. This guide covers the four data flows the portal needs, the 2026 REST option that replaces the RESTlet-only design most teams still build, and the one native NetSuite feature — Customer Center — worth ruling out before building any of it.

Contents

Portal Data Architecture

Four things happen between a WooCommerce B2B portal and NetSuite, and only one of them is a write. Mapping each portal element to its NetSuite record first prevents the common mistake of treating all four as the same kind of call — they are not, and they don’t share a governance profile.

Portal element NetSuite record Direction
My Invoices page Invoice (transaction.type = 'CustInvc') Read
Account Overview widget Customer (creditlimit, balance fields) Read
Order History page Sales Order + local WooCommerce order Read
Pay Invoice Customer Payment (apply sublist) Write

Verdict: the three reads can be satisfied from a single call, covered next. The write is a separate concern with its own record type, its own REST payload shape, and its own failure mode — covered later in this guide.

One REST Call vs. Three RESTlet Endpoints

The straightforward way to build the three read pages is three RESTlet endpoints — one for invoices, one for account/credit, one for order history — each running SuiteScript to look up and shape the data. That’s a working design, and it’s also the more expensive one to run and maintain. NetSuite’s REST web services expose a SuiteQL query endpoint that can join the transaction and customer tables in a single call, returning invoices and credit data together without any custom SuiteScript at all.

Portal read path before and after consolidating to one SuiteQL call Before: the WooCommerce portal makes three separate RESTlet GET calls per page load, one each for invoices, account credit, and order history. After: a single SuiteQL POST to the REST query endpoint returns invoices, credit, and order history together in one call. Invoice payment write-back still requires a separate call in both versions. BEFORE — three RESTlet calls per portal page load WooCommerce Portal RESTlet GET My Invoices page WooCommerce Portal RESTlet GET Account Overview (credit) WooCommerce Portal RESTlet GET Order History page AFTER — one SuiteQL call per portal page load WooCommerce Portal SuiteQL POST Invoices + credit + order history one /query/v1/suiteql response Invoice payment write-back still needs its own call in both versions — see below. softxone.com

Oracle documents the SuiteQL REST endpoint as a POST to /services/rest/query/v1/suiteql, requiring a Prefer: transient header, with the query passed in a q field. Never concatenate a customer ID straight into that string — bind it instead. NetSuite added bound-parameter support for REST SuiteQL in the 2026.2 release; the practitioner-documented syntax uses ? placeholders mapped positionally to a params array (Oracle’s own reference page does not yet show the parameter syntax explicitly, so treat this pattern as corroborated by SuitePacific’s documented example rather than a verbatim Oracle quote):

POST https://ACCOUNT.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql
Prefer: transient
Content-Type: application/json

{
  "q": "SELECT t.id, t.tranid, t.duedate, t.foreigntotal, c.creditlimit, c.balance FROM transaction t JOIN customer c ON c.id = t.entity WHERE t.type = 'CustInvc' AND t.entity = ?",
  "params": ["4"]
}

One response now carries the open invoice rows and the customer’s credit fields together. Order history follows the same pattern with a second query joining transaction filtered to sales orders, or a UNION if a single response is worth the extra query complexity.

Dimension Three RESTlet endpoints One SuiteQL call
Calls per page load 3 1 (2 with order history split out)
Code to maintain 3 script deployments, versioned and owned None — a query string
Governance cost Up to 5,000 units per call, ×3 Not RESTlet-governed
Concurrency draw 3 requests against the shared pool 1 request against the shared pool
Custom field-level masking Yes — script controls exactly what returns No — response mirrors the query

Verdict: default the read path to SuiteQL. Keep a RESTlet only if a specific field needs to be redacted or transformed before it reaches WooCommerce — something a plain query can’t do, since it returns exactly the columns it selects. This refines, not contradicts, the general REST-vs-RESTlet decision framework covered elsewhere on this site: the REST Record API can’t return aggregated data from more than one record in a single call, but SuiteQL is a separate REST surface built specifically for that — the gap that framework attributes to RESTlets doesn’t apply once SuiteQL is the tool being compared.

Credit Limit Enforcement at Checkout

When a B2B customer places a new order, WooCommerce must check whether the order total would exceed available credit in NetSuite before allowing checkout to complete. This requires a synchronous API call during WooCommerce’s checkout validation — not a cached value from a previous sync.

Cache carefullyCredit limit data can be cached with a short TTL — but not too long:

A live NetSuite call on every cart page load is too slow (adds 800–1500ms). Cache the credit data per customer for 5 minutes in a WooCommerce transient. Make a fresh call only at the moment of checkout order submission. A 5-minute cache means a customer whose credit limit was just reduced by a payment received during their session may see a stale value — acceptable for most B2B use cases.

NetSuite’s own definition of the Credit Limit field: “A credit limit defines the maximum amount the customer is allowed to accrue in outstanding receivables,” documented against the creditlimit field on the customer record’s Financial subtab. NetSuite does not expose a single “available credit” field over REST — an integration derives it by reading creditlimit and balance from the same customer record (or the same SuiteQL row, per the query above) and computing the difference itself.

Why NetSuite’s Own Credit Hold Isn’t Enough at Checkout

NetSuite doesn’t wait for an integration to catch an over-limit order. Oracle’s own documentation states plainly: “When the customer’s preset credit limit is exceeded, the credit hold is applied automatically. The customer must settle one or more outstanding invoices to reduce the balance due and release the credit hold.” That sounds like it should make the WooCommerce-side pre-check redundant. It doesn’t, for one specific timing reason.

NetSuite’s automatic credit hold fires when a Sales Order is created or updated against a customer whose balance already exceeds the limit — inside NetSuite, at the moment NetSuite processes the transaction. If the integration syncs the WooCommerce order to NetSuite asynchronously (the common pattern, covered in the architecture section above), the customer has already seen “Order confirmed” in the browser before that hold ever fires. The order then sits on hold in NetSuite, invisible to the buyer, until someone on the finance team notices and follows up — the exact support-ticket-generating failure the checkout-time check exists to prevent. NetSuite’s hold is a real safety net for orders entered any other way (phone, EDI, a saved search catching a manual entry); it is not a substitute for checking before a WooCommerce order is accepted, because by the time NetSuite’s own check runs, the portal has already told the buyer they succeeded.

Invoice Payment Write-Back

When a customer pays an invoice through the portal, the flow is: WooCommerce processes the card through a payment gateway’s process_payment() method, the WooCommerce order is marked paid, and the integration creates a NetSuite Customer Payment record applied against the specific open Invoice. Do not round the amount — partial payments must match to the cent, and NetSuite tracks the applied amount per invoice line, not as a lump sum against the customer.

Oracle’s REST record documentation for Customer Payment shows the exact shape: a payment record carries an apply sublist, and each line in that sublist names the invoice being paid and the amount applied to it.

PATCH /services/rest/record/v1/customerpayment/{paymentId}

{
  "apply": {
    "items": [
      { "doc": { "id": "104" }, "amount": 200.00, "apply": true }
    ]
  }
}

doc.id is the Invoice’s internal ID, not its document number — resolve that ID from the same SuiteQL read used to populate the My Invoices page, so the portal never has to ask the buyer for it. Setting apply to false (or the amount to 0) unapplies a line, which is the correct way to handle a payment reversal rather than deleting and recreating the Customer Payment record.

Build vs. NetSuite’s Native Customer Center

Before scoping a custom WooCommerce portal, it’s worth ruling out NetSuite’s own answer to the same problem. NetSuite ships a Customer Center role — enabled through the “Customer Access” feature — that, per Oracle’s documentation, “lets customers view their estimates, orders, invoices, and payments,” with per-task permissions an administrator can adjust (for example, setting Customer Payments to “None” instead of “Edit” if self-service payment isn’t wanted). It costs nothing beyond a NetSuite license and needs no integration code at all.

Dimension Custom WooCommerce portal NetSuite Customer Center
Where the buyer logs in Your storefront, your domain A NetSuite-hosted login, separate from your store
Branding & checkout UX Fully yours — matches the storefront NetSuite’s own UI, customizable but not storefront-native
Negotiated B2B pricing Lives in WooCommerce, applied at checkout Lives in NetSuite; the buyer sees NetSuite’s price book directly
Engineering to build & maintain Real — the read/write paths this guide covers None — a role and a feature flag
New-order placement in the same flow as invoices/credit Yes, one cart-to-checkout flow Estimates/orders viewable; not a storefront cart

Verdict: Customer Center is the right call when the audience already thinks of itself as “logging into NetSuite” — internal reps, a small distributor list, or a stopgap before a real portal ships. A custom WooCommerce portal earns its engineering cost when the buyer needs one branded storefront that combines browsing, negotiated pricing, checkout, and account status without ever seeing NetSuite’s own interface — which is the majority of the B2B ecommerce cases this guide assumes.

What Breaks in Month Two

The portal launches, the pilot customers are happy, and the failures that show up next are different from the ones caught in testing.

  • Load-test the read path at flash-sale concurrency, not demo volume — a Standard tier’s 5-connection pool chokes on portal traffic long before checkout itself does.
  • Confirm the 5-minute credit cache actually expires. A stuck WooCommerce transient means credit checks silently stop hitting NetSuite at all, and nobody notices until a customer is blocked (or not blocked) incorrectly.
  • Verify the checkout-time credit call and NetSuite’s own automatic credit hold agree. A customer blocked at checkout but not on hold in NetSuite — or on hold in NetSuite with no WooCommerce-side block — means the two checks have drifted.
  • Confirm invoice payment write-back sets apply against the correct invoice line, not just the correct invoice — partial payments key off doc and line together, not doc alone.
  • Recheck OAuth 2.0 M2M certificate expiry. An expired certificate takes down every portal page at once, not just the write path.
  • Confirm SuiteQL and RESTlet governance headroom after any bulk NetSuite import — a catalog or price-list import can exhaust the same shared concurrency pool the portal depends on.
  • Audit who still has the Customer Center role provisioned. A departed contact’s login is a live credit-limit read path into the books, independent of anything the WooCommerce portal controls.

Not sure whether your portal’s read path is over-built on RESTlets?

An ecommerce sync audit checks which interface each of your NetSuite calls actually needs — governance, concurrency, and whether NetSuite’s own credit hold and your checkout check still agree.

See what a sync audit covers →

Get the working checklists

The runbooks and decision checklists from these guides, as printable PDFs — free in the SoftXone guide library.

Browse the guide library →

Sources & Further Reading

References

  1. Managing Customer Credit Limits and HoldsOracle NetSuite Help — the Credit Limit field definition and the automatic credit hold behavior.
  2. Giving Customers AccessOracle NetSuite Help — the Customer Center role, its capabilities, and how it’s enabled.
  3. Executing SuiteQL Queries Through REST Web ServicesOracle NetSuite Help — the REST SuiteQL endpoint, method, header, and request body format.
  4. Customer PaymentOracle NetSuite Help — the REST record’s apply sublist JSON shape used for invoice payment write-back.
  5. WooCommerce Payment Gateway APIWooCommerce developer docs — the process_payment() hook that triggers the NetSuite write-back on successful payment.

Frequently asked questions

What data does a B2B portal need from NetSuite?

Invoices, credit limits and order history — enough for the customer to self-serve without exposing unrelated account data.

How is a credit limit enforced at checkout?

By checking available credit against NetSuite at checkout time rather than trusting a cached value, since NetSuite’s own automatic credit hold only fires once the order reaches NetSuite — after WooCommerce has already told the buyer checkout succeeded.

Can customers pay invoices in the portal?

Yes. The payment gateway processes the charge, then the integration creates a NetSuite Customer Payment record with an apply sublist entry naming the invoice and the amount applied to it.

Does a custom WooCommerce portal duplicate NetSuite’s own Customer Center?

Only if the goal is the same one. Customer Center is free and needs no integration code, but it’s a NetSuite-hosted login separate from the storefront — a custom portal earns its cost when the buyer needs one branded cart-to-checkout flow with negotiated pricing built in.

Should portal reads use a RESTlet or SuiteQL?

Default to a single SuiteQL query for reads — it returns invoices, credit, and order history together with no custom SuiteScript to maintain. Keep a RESTlet only if a specific field needs masking or transforming before it reaches WooCommerce.

Related guides

Discussion

Leave a Reply

Your email address will not be published. Required fields are marked *


Ship it

Need this in your stack?

We build, integrate, and ship — no calls, just delivery.

Start a project →