Skip to main content

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).

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.
Screenshot that shows a b2b checkout with fixed payment terms

  • 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.
  1. Navigate to your function in extensions/payment-customization:

    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 object, which is the function owner. This metafield is used to store the configuration for the function.
    • Retrieves the 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

    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
    }
    }
    }
    }
    }
  3. If you're using JavaScript, then run the following command to regenerate types based on your input query:

    Terminal

    shopify app function typegen
  4. Replace the contents ofsrc/run.rs or src/run.js file with the following code:

    File

    src/run.rs

    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.

  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. For more information about global IDs, refer to Global IDs in Shopify APIs.

    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.

    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 and ownerId 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.

Screenshot that shows a B2B checkout with net payment 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.

Screenshot that shows a B2B checkout with fixed payment terms with a deposit

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.

Screenshot that shows a direct-to-consumer checkout with no payment terms

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.

Screenshot that shows a direct-to-consumer checkout with net payment terms

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


Was this page helpful?