Skip to content
HeapByte — Senior Shopify Engineering Studio

Integration · 29 October 2026

Shopify B2B ERP credit limit sync: accounts receivable ageing and exposure headroom

A boutique home decor distributor grants a wholesale trade customer a €25,000 credit limit on Net 30 payment terms. The distributor connects Exact Online to Shopify Plus B2B using a standard connector that syncs customer data once every 24 hours, writing a static credit ceiling to the customer company record. The wholesale buyer already has €18,500 in outstanding unpaid invoices in the ERP, with €7,200 past 60 days overdue. At 2:00 PM, the buyer logs into their Shopify Plus B2B account, adds €15,000 of stock to their cart, and selects 'Payment Terms: Net 30' at checkout. Because Shopify's native B2B checkout only verifies company association and not real-time ledger exposure or aging arrears, the order completes instantly. The merchant ships €15,000 of physical inventory before noticing that total debt exposure now sits at €33,500—34% over the approved credit facility.

8 min read
Written by the HeapByte engineering team

Shopify B2B ERP credit limit sync accounts receivable ageing architecture

The static credit limit illusion: why naive ERP syncs cause bad debt

Most ERP-to-Shopify integrations treat credit limits as a static property of a debtor account. An integration queries Exact Online or SAP, reads Debtor.CreditLimit = 25000, and writes that number into a Shopify CompanyLocation metafield: custom.credit_limit = 25000.

This approach creates a dangerous operational illusion. A credit limit is not a balance; it is a ceiling. What dictates whether a buyer may purchase on Net terms is available credit headroom: AvailableHeadroom = CreditLimit - TotalCurrentExposure.

When an integration only syncs the ceiling, three critical systemic failures occur: invisible overdue arrears where accounts with delinquent invoices keep ordering, batch sync lag allowing credit races across branch locations, and unposted order blindness where in-flight Shopify orders are invisible to ERP ledgers for days.

In our ERP engineering for Weldaad Order Availability and multi-channel sync for Weldaad B2B/B2C Operations, we resolved these gaps by architecting an active credit exposure reconciliation layer between Exact Online and Shopify Plus.

The four pillars of true B2B credit exposure

Calculating truthful credit capacity requires reconciling four distinct financial components across both systems.

1. ERP Open Receivables (ARInvoices): All posted sales invoices in Exact Online where OutstandingBalance > 0, representing legally binding debt.

2. In-Flight Unposted Shopify Orders: Wholesale orders created in Shopify with financial_status: 'pending' not yet posted to the ERP ledger.

3. Unapplied Credit Memos: Open credit notes or customer returns in Exact Online acting as positive credit offsets, restoring headroom.

4. Approved Credit Ceiling: The formal credit facility authorized by the finance team in the ERP debtor master record.

Every calculation must execute in integer minor currency units (cents) to eliminate single-cent floating-point rounding discrepancies against the ERP general ledger.

Ageing bucket analysis and automated credit freeze gates

Numeric headroom alone is insufficient to manage wholesale credit risk. A customer with €20,000 of available headroom who refuses to pay a €1,500 invoice from four months ago represents an acute collection hazard.

Our synchronization engine groups open receivables into four standard financial aging buckets: Current (within grace period), 1 to 30 Days Overdue (soft warning), 31 to 60 Days Overdue (serious arrears), and Over 60 Days Overdue (critical default risk).

When an account enters critical arrears, the engine immediately sets custom.credit_hold = true on the Shopify CompanyLocation, instantly suppressing Net terms at checkout regardless of how much numeric credit headroom remains.

ts
export type CreditStatus = "ACTIVE" | "WARNING_ARREARS" | "HOLD_EXCEEDED_LIMIT" | "HOLD_CRITICAL_ARREARS" | "HOLD_MANUAL";

export interface DebtorCreditProfile {
  debtorCode: string;
  companyLocationId: string;
  approvedCreditLimitCents: number;
  allowGracePeriodDays: number;
  isManualCreditFreeze: boolean;
}

export function reconcileERPCreditExposureAndHeadroom(
  profile: DebtorCreditProfile,
  invoices: Array<{ debtorCode: string; dueDate: string; outstandingBalanceCents: number }>,
  creditMemos: Array<{ debtorCode: string; unappliedAmountCents: number }>,
  unpostedOrders: Array<{ debtorCode: string; totalPriceCents: number }>,
  valuationDateStr: string
) {
  const debtorInvoices = invoices.filter((i) => i.debtorCode === profile.debtorCode);
  const debtorCreditMemos = creditMemos.filter((c) => c.debtorCode === profile.debtorCode);
  const debtorUnpostedOrders = unpostedOrders.filter((o) => o.debtorCode === profile.debtorCode);

  let openReceivablesTotalCents = 0;
  let overdueDaysMax = 0;
  let daysOver60Cents = 0;

  const valTime = new Date(valuationDateStr).getTime();

  for (const inv of debtorInvoices) {
    if (inv.outstandingBalanceCents <= 0) continue;
    openReceivablesTotalCents += inv.outstandingBalanceCents;

    const dueTime = new Date(inv.dueDate).getTime();
    const daysOverdue = Math.floor((valTime - dueTime) / (1000 * 60 * 60 * 24));

    if (daysOverdue > profile.allowGracePeriodDays) {
      if (daysOverdue > overdueDaysMax) overdueDaysMax = daysOverdue;
      if (daysOverdue > 60) daysOver60Cents += inv.outstandingBalanceCents;
    }
  }

  const unappliedCreditMemosTotalCents = debtorCreditMemos.reduce((acc, m) => acc + m.unappliedAmountCents, 0);
  const unpostedShopifyOrdersTotalCents = debtorUnpostedOrders.reduce((acc, o) => acc + o.totalPriceCents, 0);

  const totalNetExposureCents = openReceivablesTotalCents + unpostedShopifyOrdersTotalCents - unappliedCreditMemosTotalCents;
  const availableHeadroomCents = Math.max(0, profile.approvedCreditLimitCents - totalNetExposureCents);

  let creditStatus: CreditStatus = "ACTIVE";
  let isCreditHold = false;

  if (profile.isManualCreditFreeze) {
    creditStatus = "HOLD_MANUAL";
    isCreditHold = true;
  } else if (daysOver60Cents > 0) {
    creditStatus = "HOLD_CRITICAL_ARREARS";
    isCreditHold = true;
  } else if (totalNetExposureCents > profile.approvedCreditLimitCents) {
    creditStatus = "HOLD_EXCEEDED_LIMIT";
    isCreditHold = true;
  } else if (overdueDaysMax > 0) {
    creditStatus = "WARNING_ARREARS";
  }

  return {
    debtorCode: profile.debtorCode,
    companyLocationId: profile.companyLocationId,
    availableHeadroomCents,
    totalNetExposureCents,
    creditStatus,
    isCreditHold,
  };
}
In our earlier Exact Online integration for Weldaad, the credit calculation summed open invoices from the ARInvoices REST endpoint but failed to account for open orders placed via Shopify in the last two hours that had not yet been processed by the ERP's nightly billing cycle. During a peak wholesale trade fair, an interior design firm placed two consecutive €8,000 orders twenty minutes apart; both passed the credit check because neither order saw the other in the ERP ledger. The production engine maintains an ephemeral pending order commitment buffer, deducting unposted Shopify orders from available headroom in real time before releasing B2B checkout terms.

In-flight order buffering: eliminating concurrent checkout double-spend

The most common vector for wholesale credit breaches is the ERP posting window.

When a B2B customer places an order on Shopify, the order status is financial_status: 'pending'. The order is not yet an invoice in Exact Online: warehouse staff must pick the items, generate an outbound delivery (GoodsDelivery), and post the billing document (SalesInvoice).

Depending on warehouse operational volume, this posting cycle can take between 4 hours and 3 days. If your integration only queries Exact Online receivables, that in-flight €12,000 order is completely invisible to subsequent checkouts.

To eliminate this vulnerability, the sync engine implements two-tier transaction buffering: webhook ingestion on orders/create into an ephemeral unposted order buffer, headroom deduction during active checks, and buffer eviction once Exact Online posts the corresponding invoice.

Shopify B2B enforcement: CompanyLocation metafields and checkout gating

Once credit headroom and hold statuses are reconciled, the engine synchronises the results into Shopify Plus B2B entities using the GraphQL Admin API mutation metafieldsSet.

The payload sets six structured metafields on the CompanyLocation: b2b_credit.available_headroom_cents, total_net_exposure_cents, credit_hold, credit_status, overdue_invoices_count, and oldest_overdue_days.

In modern Shopify Plus, frontend JavaScript cannot be trusted to block checkout. The enforcement occurs within Shopify's WebAssembly checkout sandbox using a Payment Customization Function (purchase.payment-customization.run). The function inspects the authenticated buyer's CompanyLocation metafields, hiding Net 30/Net 60 if credit_hold is true or if the current order exceeds remaining headroom.

For deeper background on configuring Checkout Extensibility rules, consult our guide on Shopify payment customization B2B rules. If an edge case order slips through, an automated hold is applied via the Shopify Fulfillment Orders API.

When this needs an engineer

You do not need a custom ERP credit limit synchronization pipeline if your B2B customers pay upfront via credit card, if your credit limits are small and non-critical, or if your wholesale volume is low enough that finance manually reviews every purchase order prior to fulfillment.

You need an engineer when trade purchasing is automated: when wholesale buyers submit high-value orders across payment terms; when outstanding ERP accounts receivable balances must dictate real-time checkout eligibility; when unposted in-flight orders cause credit line double-spending; and when delinquent invoices past 60 days must automatically freeze ordering terms without manual intervention.

Explore our engineering services in Shopify ERP and CRM integration, inspect our ERP integration case study collection, review our patterns for releasing ERP backorders safely, or examine how operational order data was structured in the Weldaad Order Availability Dashboard case study.

Send us your ERP accounting schema, payment terms structure, and debtor volume. We will architect a clean, automated credit limit synchronization pipeline that protects your working capital while accelerating trade orders.

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.