Tom Sailors
· Refreshed
Brief · Anonymized case study

B2B Subscriptions for Recurring Wholesale Orders

A wholesale brand on Plus needed to build a bridge between Subscriptions and B2B Companies. The pattern: map each buyer company to a subscription plan, store location-level pricing rules in metadata, intercept each renewal to recalculate SKU pricing per buyer contract, and route the order to an invoicing workflow instead of a payment processor. The result was recurring wholesale contracts at negotiated rates, with net terms intact.

A mid-market wholesale brand on Shopify Plus needed to enable recurring reorders for professional buyers—salons, clinics, and stockists—each operating under their own negotiated contract pricing. Native Subscriptions lack B2B company catalog support and site-level company pricing; native B2B has company pricing and net terms but no subscription functionality. The two Shopify features operate independently, leaving no path for recurring orders at company-location pricing with net-30 or net-60 invoicing workflows instead of card charges.
Four pieces
data model

Company-Linked Subscription Plans

Create a subscription plan per buyer company (or per contract tier) rather than per product. Store the buyer's company ID, location, and contract terms in Subscriptions metafield. Link each plan to a specific B2B company or location so renewals know which pricing rules to apply.

Shopify Subscriptions, B2B Companies, Metafields API
Admin GraphQL (Subscriptions API) graphql
mutation CreateSubscriptionPlan($input: SubscriptionBillingPolicyInput!) {
  subscriptionBillingPolicyCreate(input: $input) {
    userErrors { field message }
    subscriptionBillingPolicy {
      id
      name
    }
  }
}

input SubscriptionBillingPolicyInput {
  name: "Acme Salon — Net 30"
  billingCycles: 1
  interval: MONTH
  intervalCount: 1
  maxCycles: null
  metafields: [
    {
      namespace: "custom"
      key: "company_id"
      value: "gid://shopify/Company/12345"
      type: "single_line_text_field"
    }
    {
      namespace: "custom"
      key: "location_id"
      value: "gid://shopify/CompanyLocation/67890"
      type: "single_line_text_field"
    }
    {
      namespace: "custom"
      key: "contract_terms"
      value: "net_30"
      type: "single_line_text_field"
    }
  ]
}
Store company and location references as metafield values. These link back to B2B company records and enable pricing lookups at renewal.
webhook handler

Renewal Price Recalculation Hook

On each subscription renewal (billing_attempt.challenged or processing), read the order's metafield company ID and location ID. Query B2B Companies pricing rules for that company-location pair. Rewrite the renewal order line items to reflect contract pricing before it's finalized.

Webhooks (Subscriptions), B2B GraphQL API, Metafields
Webhook endpoint (Node.js / similar) javascript
async function recalculatePricingOnRenewal(webhookData) {
  const subscriptionContract = webhookData.subscription_contract;
  const companyId = subscriptionContract.metafields?.find(m => m.key === 'company_id')?.value;
  const locationId = subscriptionContract.metafields?.find(m => m.key === 'location_id')?.value;

  if (!companyId || !locationId) return;

  // Query B2B company pricing
  const pricingQuery = `
    query {
      company(id: "${companyId}") {
        locations(first: 10) {
          edges {
            node {
              id
              catalogPublications {
                edges {
                  node {
                    catalog {
                      publications(first: 5) {
                        edges {
                          node {
                            priceListAssignments {
                              edges {
                                node {
                                  priceList {
                                    priceListItems(first: 100) {
                                      edges {
                                        node {
                                          variant { id sku }
                                          price
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  `;

  const pricingResponse = await shopifyGraphQL(pricingQuery);
  const location = pricingResponse.data.company.locations.edges.find(
    edge => edge.node.id === locationId
  )?.node;

  // Build price map from B2B catalog pricing
  const priceMap = {};
  location?.catalogPublications?.edges?.forEach(pubEdge => {
    pubEdge.node.catalog.publications.edges.forEach(catalogEdge => {
      catalogEdge.node.publications.priceListAssignments.edges.forEach(assignEdge => {
        assignEdge.node.priceList.priceListItems.edges.forEach(itemEdge => {
          const variantId = itemEdge.node.variant.id;
          priceMap[variantId] = itemEdge.node.price;
        });
      });
    });
  });

  // Update order line items with B2B pricing
  return { priceMap, companyId, locationId };
}
Fetch B2B pricing on renewal. Map variant IDs to contract prices. Use this to rewrite line items before order finalization.
order post-processing

Net-30 Invoicing Redirect

After a subscription renewal creates an order, check the contract metafield for invoicing terms (net_30, net_60, etc.). If present, mark the order as pending invoice (using a custom metafield status), hold it from payment processing, and route it to an invoicing app (Vin, Zoho, or a custom invoice service) instead of charging a payment method.

Order API, Webhooks, Invoicing integration
Order webhook handler or workflow automation javascript
async function redirectToInvoicing(orderId, companyId, contractTerms) {
  const orderData = {
    id: orderId,
    metafields: [
      {
        namespace: "custom",
        key: "billing_mode",
        value: "invoice",
        type: "single_line_text_field"
      },
      {
        namespace: "custom",
        key: "invoice_terms",
        value: contractTerms, // e.g., "net_30"
        type: "single_line_text_field"
      },
      {
        namespace: "custom",
        key: "company_id",
        value: companyId,
        type: "single_line_text_field"
      }
    ]
  };

  // Update order metafield
  await shopifyGraphQL(`
    mutation UpdateOrder($input: OrderInput!) {
      orderUpdate(input: $input) {
        order { id metafields { namespace key value } }
        userErrors { field message }
      }
    }
  `, { input: orderData });

  // Send to invoicing webhook
  await fetch('https://invoicing-service.example.com/webhook', {
    method: 'POST',
    body: JSON.stringify({
      shopify_order_id: orderId,
      company_id: companyId,
      terms: contractTerms,
      action: 'create_invoice'
    })
  });
}
Set custom order status and metafield to signal invoicing workflow. Do not run payment charge; instead trigger external invoicing system via webhook.
storefront UI

Company Portal Subscription Management

Build a B2B company portal screen (or extend existing account dashboard) to show active subscription contracts. Display renewal dates, contract pricing, and linked SKUs. Allow location managers to adjust quantities before renewal or update invoicing contact details without changing the underlying subscription contract.

Remix / hydrogen, B2B Companies API, Subscriptions API, custom CSS
Hydrogen or custom Remix route (/account/subscriptions) javascript
// Subscription contract portal component (Hydrogen/Remix)
export async function loader({ context, params }) {
  const { company } = params;
  const contractsQuery = `
    query {
      subscriptionContracts(first: 10, query: "metafield:custom.company_id:'${company}'") {
        edges {
          node {
            id
            status
            lines {
              id
              productVariant { title sku }
              quantity
              price
            }
            nextBillingDate
            metafields(first: 10) {
              edges {
                node { key value namespace }
              }
            }
          }
        }
      }
    }
  `;

  const data = await context.queryAPI(contractsQuery);
  return { contracts: data.subscriptionContracts.edges };
}

export default function ContractPortal({ contracts }) {
  return (
    <div>
      <h1>Your Subscription Contracts</h1>
      {contracts.map(({ node: contract }) => (
        <div key={contract.id} className="contract-card">
          <h3>{contract.nextBillingDate}</h3>
          <ul>
            {contract.lines.map(line => (
              <li key={line.id}>
                {line.productVariant.title} × {line.quantity} @ ${line.price}
              </li>
            ))}
          </ul>
          <button onClick={() => openAmendModal(contract.id)}>Adjust Next Order</button>
        </div>
      ))}
    </div>
  );
}
Query subscription contracts filtered by company ID metafield. Display terms, SKUs, and quantities. Offer quick adjustments before renewal; amendments save to the contract.

Got a similar problem?

Sketch your build in 30 seconds — voice, type, or attach a screenshot.

Sketch the build →