Set payment terms
Payment terms enable buyers to pay for their orders at a later date instead of immediately at checkout. This tutorial shows you how to use the paymentTermsSet
operation in the Payment Customization API to dynamically set payment terms based on business rules.
Anchor to Understanding payment termsUnderstanding payment terms
Payment terms allow merchants to offer flexible payment options to their customers:
- Fixed payment terms: Set a specific due date for payment (for example, payment due by January 31st).
- Net payment terms: Set the payment to be due a certain number of days after the order is placed (for example, "net 30" means that payment is due 30 days after the order is placed).
- Event payment terms: Set the payment as due when a specific event occurs:
FULFILLMENT_CREATED
: Due when individual items are fulfilled.ORDER_FULFILLED
: Due when the entire order is fulfilled.INVOICE_SENT
: Due when the invoice is sent.
- Deposits: Require a percentage upfront while deferring the remainder (for example, 25% deposit due now, remaining 75% within 30 days).
Anchor to What you'll learnWhat you'll learn
In this tutorial, you'll learn how to do the following tasks:
- Set different payment terms on a checkout based on the buyer identity and the total value of the cart.
- Set net, fixed or event payment terms with an optional deposit for a buyer.

Anchor to RequirementsRequirements
- You've completed the previous tutorials in this series.
Anchor to Step 1: Configure the functionStep 1: Configure the function
In this example, payment terms are set based on the cart total and whether it's a B2B order. You'll implement the following behaviour:
- Payment terms won't be modified for any order where the cart total is less than $500.
- If the cart total is $500 or more:
- B2B orders will have fixed terms with a 50% deposit.
- Direct-to-consumer (D2C) orders won't have their payment terms modified.
-
Navigate to your function in
extensions/payment-customization
:cd extensions/payment-customization -
Replace the code in the
src/run.graphql
file with the following code.The input query performs the following actions:
- Retrieves a metafield from the
paymentCustomization
object, which is the function owner. This metafield is used to store the configuration for the function. - Retrieves the
cart
object, which includes acost
object with the total value of the cart, andbuyerIdentity
that can be used to determine if the buyer is B2B.
The query differs slightly in Rust and JavaScript due to code generation requirements.
run.graphql
src/run.graphql
query Input {paymentCustomization {metafield(namespace: "$app", key: "payment-customization-function-configuration") {jsonValue}}cart {cost {totalAmount {amount}}buyerIdentity{purchasingCompany{company {id}}}}}query RunInput {paymentCustomization {metafield(namespace: "$app", key: "payment-customization-function-configuration") {jsonValue}}cart {cost {totalAmount {amount}}buyerIdentity{purchasingCompany{company {id}}}}}query Input { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } }
query RunInput { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } }
- Retrieves a metafield from the
-
If you're using JavaScript, then run the following command to regenerate types based on your input query:
Terminal
shopify app function typegen -
Replace the contents of
src/run.rs
orsrc/run.js
file with the following code:File
src/run.rs
use crate::schema;use shopify_function::prelude::*;use shopify_function::Result;pub struct Configuration {min_for_payment_terms: String,due_at_for_fixed_terms: String,}fn run(input: schema::run::Input) -> Result<schema::FunctionRunResult> {let no_changes = schema::FunctionRunResult { operations: vec![] };let configuration: &Configuration= match input.payment_customization().metafield() {Some(metafield) => metafield.json_value(),None => return Ok(no_changes),};if configuration.min_for_payment_terms.is_empty() || configuration.due_at_for_fixed_terms.is_empty() {return Ok(no_changes);}let min_for_payment_terms = match configuration.min_for_payment_terms.parse::<f64>() {Ok(val) => val,Err(_) => 0.0,};let cart_total = input.cart().cost().total_amount().amount().to_string().parse::<f64>().unwrap_or(0.0);if cart_total < min_for_payment_terms {return Ok(no_changes);}let mut is_b2b = false;if let Some(buyer_identity) = &input.cart().buyer_identity() {if let Some(_) = &buyer_identity.purchasing_company() {// Check if purchasing company existsis_b2b = true;}}if !is_b2b {return Ok(no_changes);}let operations = vec![schema::Operation::PaymentTermsSet(schema::PaymentTermsSetOperation {payment_terms: Some(schema::PaymentTerms::Fixed(schema::FixedPaymentTerms {deposit: Some(schema::Deposit {percentage: 50.0,}),due_at: configuration.due_at_for_fixed_terms.clone(),})),})];Ok(schema::FunctionRunResult { operations })}// @ts-check/*** @typedef {import("../generated/api").RunInput} RunInput* @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult*//*** @type {FunctionRunResult}*/const NO_CHANGES = {operations: [],};/*** Configuration schema for payment terms function* @typedef {Object} Configuration* @property {number} minForPaymentTerms - Minimum cart total required to apply payment terms* @property {Date} dueAtForFixedTerms - Due date for fixed payment terms*//*** Parses configuration from metafield or returns null if invalid* @param {RunInput} input - Function input* @returns {Configuration|null} Parsed configuration or null if invalid*/function getConfiguration(input) {const config = input?.paymentCustomization?.metafield?.jsonValue;if (!config) {console.log("No configuration metafield found");return null;}if (!config.minForPaymentTerms || !config.dueAtForFixedTerms) {console.log("Missing required configuration values");return null;}return {minForPaymentTerms: parseFloat(config.minForPaymentTerms),dueAtForFixedTerms: new Date(config.dueAtForFixedTerms),};}/*** Checks if the buyer is a B2B customer* @param {RunInput} input - Function input* @returns {boolean} True if the buyer is a B2B customer*/function isB2BCustomer(input) {return Boolean(input.cart.buyerIdentity?.purchasingCompany);}/*** Sets payment terms based on cart total and buyer identity* @param {RunInput} input - Function input* @returns {FunctionRunResult} Function result with operations to apply*/export function run(input) {const configuration = getConfiguration(input);if (!configuration) {return NO_CHANGES;}const cartTotal = parseFloat(input.cart.cost.totalAmount.amount);if (cartTotal < configuration.minForPaymentTerms) {console.error(`Cart total ${cartTotal} is below minimum ${configuration.minForPaymentTerms}`);return NO_CHANGES;}if (!isB2BCustomer(input)) {console.log('Not a B2B customer, no changes to payment terms');return NO_CHANGES;}const dueAtForFixedTerms = configuration.dueAtForFixedTerms;console.log(`Setting fixed terms for B2B customer, due at: ${dueAtForFixedTerms}`);return {operations: [{paymentTermsSet: {paymentTerms: {fixed: {deposit: {percentage: 50},dueAt: dueAtForFixedTerms}}}}]};}use crate::schema; use shopify_function::prelude::*; use shopify_function::Result; #[derive(Deserialize, Default, PartialEq)] #[shopify_function(rename_all = "camelCase")] pub struct Configuration { min_for_payment_terms: String, due_at_for_fixed_terms: String, } #[shopify_function] fn run(input: schema::run::Input) -> Result<schema::FunctionRunResult> { let no_changes = schema::FunctionRunResult { operations: vec![] }; let configuration: &Configuration= match input.payment_customization().metafield() { Some(metafield) => metafield.json_value(), None => return Ok(no_changes), }; if configuration.min_for_payment_terms.is_empty() || configuration.due_at_for_fixed_terms.is_empty() { return Ok(no_changes); } let min_for_payment_terms = match configuration.min_for_payment_terms.parse::<f64>() { Ok(val) => val, Err(_) => 0.0, }; let cart_total = input.cart().cost().total_amount().amount().to_string().parse::<f64>().unwrap_or(0.0); if cart_total < min_for_payment_terms { return Ok(no_changes); } let mut is_b2b = false; if let Some(buyer_identity) = &input.cart().buyer_identity() { if let Some(_) = &buyer_identity.purchasing_company() { // Check if purchasing company exists is_b2b = true; } } if !is_b2b { return Ok(no_changes); } let operations = vec![schema::Operation::PaymentTermsSet( schema::PaymentTermsSetOperation { payment_terms: Some(schema::PaymentTerms::Fixed( schema::FixedPaymentTerms { deposit: Some(schema::Deposit { percentage: 50.0, }), due_at: configuration.due_at_for_fixed_terms.clone(), } )), } )]; Ok(schema::FunctionRunResult { operations }) }
// @ts-check /** * @typedef {import("../generated/api").RunInput} RunInput * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult */ /** * @type {FunctionRunResult} */ const NO_CHANGES = { operations: [], }; /** * Configuration schema for payment terms function * @typedef {Object} Configuration * @property {number} minForPaymentTerms - Minimum cart total required to apply payment terms * @property {Date} dueAtForFixedTerms - Due date for fixed payment terms */ /** * Parses configuration from metafield or returns null if invalid * @param {RunInput} input - Function input * @returns {Configuration|null} Parsed configuration or null if invalid */ function getConfiguration(input) { const config = input?.paymentCustomization?.metafield?.jsonValue; if (!config) { console.log("No configuration metafield found"); return null; } if (!config.minForPaymentTerms || !config.dueAtForFixedTerms) { console.log("Missing required configuration values"); return null; } return { minForPaymentTerms: parseFloat(config.minForPaymentTerms), dueAtForFixedTerms: new Date(config.dueAtForFixedTerms), }; } /** * Checks if the buyer is a B2B customer * @param {RunInput} input - Function input * @returns {boolean} True if the buyer is a B2B customer */ function isB2BCustomer(input) { return Boolean(input.cart.buyerIdentity?.purchasingCompany); } /** * Sets payment terms based on cart total and buyer identity * @param {RunInput} input - Function input * @returns {FunctionRunResult} Function result with operations to apply */ export function run(input) { const configuration = getConfiguration(input); if (!configuration) { return NO_CHANGES; } const cartTotal = parseFloat(input.cart.cost.totalAmount.amount); if (cartTotal < configuration.minForPaymentTerms) { console.error(`Cart total ${cartTotal} is below minimum ${configuration.minForPaymentTerms}`); return NO_CHANGES; } if (!isB2BCustomer(input)) { console.log('Not a B2B customer, no changes to payment terms'); return NO_CHANGES; } const dueAtForFixedTerms = configuration.dueAtForFixedTerms; console.log(`Setting fixed terms for B2B customer, due at: ${dueAtForFixedTerms}`); return { operations: [ { paymentTermsSet: { paymentTerms: { fixed: { deposit: { percentage: 50 }, dueAt: dueAtForFixedTerms } } } } ] }; }
Anchor to Step 2: Populate the payment customization configuration metafieldStep 2: Populate the payment customization configuration metafield
To populate the configuration metafield, first use the paymentCustomizations
query to confirm the payment customization ID, and then use the metafieldsSet
mutation to populate the same metafield that you specified in the input query.
-
Open the local GraphiQL interface bundled in the Shopify CLI. Run
shopify app dev
to start your development server, then pressg
to open GraphiQL. -
In the GraphiQL interface, in the API Version field, select the 2025-07 version or later.
-
Execute the following query, and make note of the
id
value of the payment customization that you created in the previous tutorial. For more information about global IDs, refer to Global IDs in Shopify APIs.query {paymentCustomizations(first: 100) {edges {node {idtitle}}}} -
Execute the following mutation, replacing
YOUR_CUSTOMIZATION_ID_HERE
with the full global ID of your payment customization.The value of the metafield specifies that the minimum cart total to set payment terms is $500, and the due date for fixed terms is November 15, 2025 EST.
mutation {metafieldsSet(metafields: [{ownerId: "YOUR_CUSTOMIZATION_ID_HERE"namespace: "$app"key: "payment-customization-function-configuration"value: "{ \"minForPaymentTerms\":500,\"dueAtForFixedTerms\":\"Nov 15 2025 00:00:00 GMT-0500\"}"type: "json"}]) {metafields {id}userErrors {message}}}You should receive a GraphQL response that includes the ID of the created metafield. If the response includes any messages under
userErrors
, then review the errors, check that your mutation andownerId
are correct, and try the request again.
Anchor to Step 3: Test the payment customizationStep 3: Test the payment customization
Test how payment terms are set based on the buyer identity and cart total.
Anchor to Test with a B2B checkout with a cart total under $500Test with a B2B checkout with a cart total under $500
Open your development store and log in as a B2B customer. Build a cart with a total (including shipping and tax) under $500. The function doesn't set any payment terms. The checkout uses the existing payment terms of the company location. In this case, the company location has net 45 terms.

Anchor to Test with a B2B checkout with a cart total over $500Test with a B2B checkout with a cart total over $500
Open your development store and log in as a B2B customer. Build a cart with a total (including shipping and tax) over $500. The function sets fixed payment terms with a 50% deposit. The checkout uses the fixed payment terms.

Anchor to Test with a direct-to-consumer checkoutTest with a direct-to-consumer checkout
Open your development store and log in as a direct-to-consumer customer. Build a cart with a total (including shipping and tax) over $500. The function doesn't set any payment terms, so it doesn't modify the checkout.

Anchor to Considerations for direct-to-consumer checkouts with payment termsConsiderations for direct-to-consumer checkouts with payment terms
If you choose to implement payment terms for direct-to-consumer buyers, there are several important considerations:
Direct-to-consumer checkouts with payment terms don't support any pay now options. Buyers will only see the option to defer their payment. Additional deferred payment options (vaulted credit cards) are available for B2B.

If you set fulfillment event payment terms on a checkout and the Automatically when fulfilling payment capture setting is enabled, the payment terms are captured according to the payment term set by the function:
FULFILLMENT_CREATED
: Payment is captured per fulfillment.ORDER_FULFILLED
: Payment is captured when the entire order is fulfilled.

Anchor to Next stepsNext steps
- Build a payment customization user interface with App Bridge.
- Learn how to use variables in your input query.
- Explore more payment customization options in the API reference.