--- title: Set payment terms description: Set payment terms on a checkout based on the buyer identity and cart total. source_url: html: 'https://shopify.dev/docs/apps/build/checkout/payments/payment-terms' md: 'https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md' --- ExpandOn this page * [Understanding payment terms](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#understanding-payment-terms) * [What you'll learn](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#what-youll-learn) * [Requirements](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#requirements) * [Step 1: Configure the function](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-1-configure-the-function) * [Step 2: Set up and populate the configuration metafield](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-2-set-up-and-populate-the-configuration-metafield) * [Step 3: Test the payment customization](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-3-test-the-payment-customization) * [Considerations for direct-to-consumer checkouts with payment terms](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#considerations-for-direct-to-consumer-checkouts-with-payment-terms) * [Next steps](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#next-steps) # 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](https://shopify.dev/docs/api/functions/reference/payment-customization) to dynamically set [payment terms](https://help.shopify.com/manual/fulfillment/managing-orders/payments/deferred-payments) based on business rules. *** ## Understanding 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). *** ## What 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. * Use metafields to store configuration data for your payment customization function. ![Screenshot that shows a b2b checkout with fixed payment terms](https://shopify.dev/assets/assets/images/apps/checkout/b2b-checkout-with-fixed-payment-terms-and-deposit-CN46g4nJ.png) *** ## Requirements * You've completed the previous tutorials in this [series](https://shopify.dev/docs/apps/build/checkout/payments#tutorial-series). *** ## Step 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. 1. Navigate to your function in `extensions/payment-customization`: ```terminal cd extensions/payment-customization ``` 2. 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`](https://shopify.dev/docs/api/functions/reference/payment-customization#Input.fields.paymentCustomization) object, which is the function owner. This metafield is used to store the configuration for the function. * Retrieves the [`cart`](https://shopify.dev/docs/api/functions/reference/payment-customization#Input.fields.cart) object, which includes a `cost` object with the total value of the cart, and `buyerIdentity` 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 ```graphql query Input { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } } ``` ```graphql query RunInput { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } } ``` ##### Rust input query ``` query Input { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } } ``` ##### JavaScript input query ``` query RunInput { paymentCustomization { metafield(namespace: "$app", key: "payment-customization-function-configuration") { jsonValue } } cart { cost { totalAmount { amount } } buyerIdentity{ purchasingCompany{ company { id } } } } } ``` 3. If you're using JavaScript, then run the following command to regenerate types based on your input query: ## Terminal ```terminal shopify app function typegen ``` 4. Replace the contents of`src/run.rs` or `src/run.js` file with the following code: ## File ## src/run.rs ```rust 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 { 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::() { Ok(val) => val, Err(_) => 0.0, }; let cart_total = input.cart().cost().total_amount().amount().to_string().parse::().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 }) } ``` ```javascript // @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 } } } } ] }; } ``` ##### Rust ``` 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 { 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::() { Ok(val) => val, Err(_) => 0.0, }; let cart_total = input.cart().cost().total_amount().amount().to_string().parse::().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 }) } ``` ##### JavaScript ``` // @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 } } } } ] }; } ``` *** ## Step 2: Set up and populate the configuration metafield Before you can use configuration data in your function, you need to define and populate a metafield that stores the payment customization settings. ### Define the metafield Use a [declarative custom data definition](https://shopify.dev/docs/apps/build/custom-data/declarative-custom-data-definitions) to define the metafield that will store the payment customization configuration. This approach automatically creates the metafield definition when your app is installed. In your app's `shopify.app.toml` file, add the following metafield definition: ```toml [payment_customization.metafields.app.payment-customization-function-configuration] type = "json" name = "Payment terms configuration" description = "Configuration settings for payment terms customization function" access.admin = "merchant_read_write" ``` This creates a metafield to store configuration settings for your payment customization function, allowing you to control payment term logic dynamically. ### Populate the configuration Since the metafield definition is automatically created by your TOML configuration, you now need to populate it with the actual configuration values. First use the [`paymentCustomizations`](https://shopify.dev/docs/api/admin-graphql/current/queries/paymentCustomizations) query to confirm the payment customization ID, and then use the [`metafieldsSet`](https://shopify.dev/docs/api/admin-graphql/current/mutations/metafieldsSet) mutation to populate the metafield with configuration data. 1. Open the local GraphiQL interface bundled in the Shopify CLI. Run `shopify app dev` to start your development server, then press `g` to open GraphiQL. 2. In the GraphiQL interface, in the **API Version** field, select the **2025-07** version or later. 3. Execute the following query, and make note of the `id` value of the payment customization that you created in the [previous tutorial](https://shopify.dev/docs/apps/build/checkout/payments/create-payments-function). For more information about global IDs, refer to [Global IDs in Shopify APIs](https://shopify.dev/docs/api/usage/gids). ```graphql query { paymentCustomizations(first: 100) { edges { node { id title } } } } ``` 4. Execute the following mutation, replacing `YOUR_CUSTOMIZATION_ID_HERE` with the full global ID of your payment customization. This mutation populates the metafield (defined in your TOML configuration) with the actual configuration values. The value specifies that the minimum cart total to set payment terms is $500, and the due date for fixed terms is November 15, 2025 EST. ```graphql 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 updated metafield. The metafield definition was automatically created when you installed your app with the TOML configuration, so this mutation is just setting the values. If the response includes any messages under `userErrors`, then review the errors, check that your mutation and `ownerId` are correct, and try the request again. *** ## Step 3: Test the payment customization Test how payment terms are set based on the buyer identity and cart total. ### Test with a B2B checkout with a cart total under $500 Open your dev 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. ![Screenshot that shows a B2B checkout with net payment terms](https://shopify.dev/assets/assets/images/apps/checkout/b2b-checkout-with-net-payment-terms-Bu-Cs4zZ.png) ### Test with a B2B checkout with a cart total over $500 Open your dev 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. ![Screenshot that shows a B2B checkout with fixed payment terms with a deposit](https://shopify.dev/assets/assets/images/apps/checkout/b2b-checkout-with-fixed-payment-terms-and-deposit-CN46g4nJ.png) ### Test with a direct-to-consumer checkout Open your dev 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. ![Screenshot that shows a direct-to-consumer checkout with no payment terms](https://shopify.dev/assets/assets/images/apps/checkout/d2c-checkout-with-no-payment-terms-B7-PZNIc.png) *** ## Considerations 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. ![Screenshot that shows a direct-to-consumer checkout with net payment terms](https://shopify.dev/assets/assets/images/apps/checkout/d2c-checkout-with-net-payment-terms-VR5PR3xA.png) * 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. ![Screenshot that shows a direct-to-consumer checkout with due on fulfillment event payment terms](https://shopify.dev/assets/assets/images/apps/checkout/d2c-checkout-with-due-on-fulfillment-event-payment-terms-6D8tkTIS.png) * Checkouts with [deferred purchase options](https://shopify.dev/docs/apps/build/purchase-options/deferred) (such as pre-orders and try before you buy) follow specific rules for determining due dates and deposits when a payment customization function is used. For more information, see [Orders with multiple due dates and deposits](https://shopify.dev/docs/apps/build/purchase-options/deferred#orders-with-multiple-due-dates-and-deposits). *** ## Next steps * Build a [payment customization user interface](https://shopify.dev/docs/apps/build/checkout/payments/build-ui) with App Bridge. * Learn how to use [variables](https://shopify.dev/docs/apps/build/functions/input-queries/use-variables-input-queries) in your input query. * Explore more [payment customization options](https://shopify.dev/docs/api/functions/reference/payment-customization) in the API reference. *** * [Understanding payment terms](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#understanding-payment-terms) * [What you'll learn](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#what-youll-learn) * [Requirements](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#requirements) * [Step 1: Configure the function](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-1-configure-the-function) * [Step 2: Set up and populate the configuration metafield](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-2-set-up-and-populate-the-configuration-metafield) * [Step 3: Test the payment customization](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#step-3-test-the-payment-customization) * [Considerations for direct-to-consumer checkouts with payment terms](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#considerations-for-direct-to-consumer-checkouts-with-payment-terms) * [Next steps](https://shopify.dev/docs/apps/build/checkout/payments/payment-terms.md#next-steps)