Net payment terms for B2B buyers

A Shopify payment customization Function that sets Net 30 (or 7–90 day) payment terms and an optional deposit for B2B company buyers. Shopify Plus.

Function API
Payment Customization
Target
cart.payment-methods.transform.run
Plan
Shopify Plus (paymentTermsSet)
App scope
write_payment_customizations
API version
2026-10
Last checked

In short

This payment customization Function gives B2B company buyers net payment terms, Net 30 by default, with per-company overrides and an optional deposit. Consumers aren’t affected. It uses the paymentTermsSet operation of the cart.payment-methods.transform.run target, Payment Customization API version 2026-10. Shopify only allows paymentTermsSet on stores with the Plus plan.

How to use it

  1. Create an app with the Shopify CLI, then add the extension: shopify app generate extension --template payment_customization, with JavaScript.
  2. Replace the generated shopify.extension.toml and the two files in src/ with the files below.
  3. Run the fixtures with shopify app function run --input tests/company-default-terms.input.json.
  4. Deploy with shopify app deploy and create the customization with paymentCustomizationCreate on the Plus store.
  5. Set the $app:function-configuration metafield, for example {"dueInDays": 30, "companyDueInDays": {"gid://shopify/Company/1234567890": 60}, "depositPercentage": 20}.

Code

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Net payment terms for B2B buyers"
handle = "b2b-net-payment-terms"
type = "function"

  [[extensions.targeting]]
  target = "cart.payment-methods.transform.run"
  input_query = "src/cart_payment_methods_transform_run.graphql"
  export = "cart-payment-methods-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_payment_methods_transform_run.graphql
query CartPaymentMethodsTransformRunInput {
  cart {
    buyerIdentity {
      purchasingCompany {
        company {
          id
        }
      }
    }
  }
  paymentCustomization {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_payment_methods_transform_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartPaymentMethodsTransformRunInput} CartPaymentMethodsTransformRunInput
 * @typedef {import("../generated/api").CartPaymentMethodsTransformRunResult} CartPaymentMethodsTransformRunResult
 */

/** Net terms Shopify accepts for dueInDays in API version 2026-10. */
const SUPPORTED_DUE_IN_DAYS = [7, 15, 30, 45, 60, 90];

/** Settings stored as JSON in the customization's metafield. */
const DEFAULT_CONFIG = {
  // Terms for every company buyer without an override below.
  dueInDays: 30,
  // Company ID → days, e.g. { "gid://shopify/Company/1234567890": 60 }.
  companyDueInDays: /** @type {Record<string, number>} */ ({}),
  // Optional deposit (1–99 % of the order total) paid at checkout; null for none.
  depositPercentage: /** @type {number | null} */ (null),
};

/** @type {CartPaymentMethodsTransformRunResult} */
const NO_CHANGES = { operations: [] };

/**
 * @param {CartPaymentMethodsTransformRunInput} input
 * @returns {CartPaymentMethodsTransformRunResult}
 */
export function cartPaymentMethodsTransformRun(input) {
  const company = input.cart.buyerIdentity?.purchasingCompany?.company;
  // Consumers (no purchasing company) keep paying at checkout.
  if (!company) return NO_CHANGES;

  const config = { ...DEFAULT_CONFIG, ...input.paymentCustomization?.metafield?.jsonValue };
  const dueInDays = (config.companyDueInDays ?? {})[company.id] ?? config.dueInDays;
  // An unsupported value would make Shopify reject the operation; leave the
  // company's own terms in place instead.
  if (!SUPPORTED_DUE_IN_DAYS.includes(dueInDays)) return NO_CHANGES;

  // Shopify accepts a deposit of 1–99 %; anything else is left out rather than rejected.
  const percentage = config.depositPercentage;
  const deposit = Number.isFinite(percentage) && percentage >= 1 && percentage <= 99 ? { percentage } : null;
  return {
    operations: [{ paymentTermsSet: { paymentTerms: { net: { dueInDays, deposit } } } }],
  };
}
Test input and expected output
tests/company-default-terms.input.json
{
  "cart": {
    "buyerIdentity": { "purchasingCompany": { "company": { "id": "gid://shopify/Company/1234567890" } } }
  },
  "paymentCustomization": { "metafield": null }
}
tests/company-default-terms.output.json
{
  "operations": [
    { "paymentTermsSet": { "paymentTerms": { "net": { "dueInDays": 30, "deposit": null } } } }
  ]
}
tests/company-override-with-deposit.input.json
{
  "cart": {
    "buyerIdentity": { "purchasingCompany": { "company": { "id": "gid://shopify/Company/1234567890" } } }
  },
  "paymentCustomization": {
    "metafield": {
      "jsonValue": {
        "companyDueInDays": { "gid://shopify/Company/1234567890": 60 },
        "depositPercentage": 20
      }
    }
  }
}
tests/company-override-with-deposit.output.json
{
  "operations": [
    { "paymentTermsSet": { "paymentTerms": { "net": { "dueInDays": 60, "deposit": { "percentage": 20 } } } } }
  ]
}
tests/consumer-checkout.input.json
{
  "cart": { "buyerIdentity": { "purchasingCompany": null } },
  "paymentCustomization": { "metafield": null }
}
tests/consumer-checkout.output.json
{ "operations": [] }

Written by Lintel. MIT license: free to use in your own store and in client work.

Limits and edge cases

  • Net terms accept only 7, 15, 30, 45, 60 or 90 days in this API version; any other value is ignored and the company keeps its own terms.
  • A deposit must be between 1 and 99 percent of the order total.
  • Payment terms can’t be set when the cart has subscription items, and aren’t supported in storefront accelerated checkouts.
  • Shopify also offers event-based terms (for example due on fulfillment) and fixed due dates through the same operation; this example only sets net terms.

Questions

How do I offer Net 30 to wholesale customers on Shopify?
Use a payment customization Function that returns paymentTermsSet with net terms for company buyers, as this example does. It can also give some companies longer terms or ask for a deposit.
Does this work without Shopify Plus?
No. Shopify’s reference says paymentTermsSet is only available on stores with the Plus plan.
Can I require a deposit on large B2B orders?
Yes. Set depositPercentage and the buyer pays that share at checkout and the rest when the terms are due. To make it depend on order size, add cart.cost.totalAmount to the input query.
Can I remove payment terms for some buyers?
Yes. Returning paymentTermsSet with paymentTerms set to null removes them for that checkout, so the buyer pays upfront.
  • Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
  • Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
  • Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
  • MIT license. Use the code in your own store or in client work.

Need this built, tested and maintained for your store? See Shopify Functions development.

Have something to build? Let’s talk it through.

Taking a limited number of projects at a time. Every brief is read by an engineer, not a salesperson.

Gencer KarakayaShopify Engineer

An engineer reads your brief, and we reply by email.