Skip to main content

B2B 開発

Shopify での B2B(企業間)コマースは急速に成長しているセグメントであり、Shopify Plus はアプリ開発者が拡張・統合できるネイティブ B2B 機能を提供しています。このモジュールでは、B2B データモデル、企業アカウントとロケーション、カタログによるカスタム価格設定、発注書承認ワークフロー、チェックアウトカスタマイズ、決済方法の統合をカバーします。

Shopify Plus の B2B 機能

Shopify のネイティブ B2B 機能は Shopify Plus プラン限定で利用可能です。アプリ開発者として、これらの機能を理解することで、マーチャントが複雑な卸売オペレーションに必要とする補完ツールを構築できます。

B2B データモデルの概要

Company
├── Company Locations (shipping/billing addresses)
│ ├── Catalog Assignments (custom pricing)
│ └── Payment Terms (Net 30, Net 60, etc.)
├── Company Contacts (buyer accounts)
│ └── Roles & Permissions
└── Orders (linked to company + location)
info

B2B と DTC(消費者直販)は同じ Shopify ストア上で運用されます。1つのストアで小売顧客と卸売バイヤーの両方に対応できます。ストアフロントの体験は、ログイン中の顧客が B2B 企業に関連付けられているかどうかに基づいて適応します。

B2B 企業のクエリ

query GetCompanies($first: Int!, $query: String) {
companies(first: $first, query: $query) {
edges {
node {
id
name
externalId
note
customerSince
orderCount
totalSpent {
amount
currencyCode
}
mainContact {
customer {
firstName
lastName
email
}
}
locations(first: 10) {
edges {
node {
id
name
shippingAddress {
address1
city
province
country
zip
}
catalog {
id
title
}
buyerExperienceConfiguration {
paymentTermsTemplate {
paymentTermsType
dueInDays
}
}
}
}
}
}
}
}
}

企業アカウントとロケーション

プログラムによる企業作成

ERP システムやその他のプラットフォームから卸売顧客をインポートするアプリは、API を通じて企業を作成する必要があります:

// services/b2b-company-service.js
export async function createCompany(client, companyData) {
const response = await client.query({
data: {
query: `mutation CompanyCreate($input: CompanyCreateInput!) {
companyCreate(input: $input) {
company {
id
name
}
userErrors {
field
message
}
}
}`,
variables: {
input: {
company: {
name: companyData.name,
externalId: companyData.erpId,
note: companyData.notes,
},
companyLocation: {
name: companyData.headquarters.name || 'Headquarters',
shippingAddress: {
address1: companyData.headquarters.address1,
address2: companyData.headquarters.address2,
city: companyData.headquarters.city,
provinceCode: companyData.headquarters.state,
countryCode: companyData.headquarters.country,
zip: companyData.headquarters.zip,
},
buyerExperienceConfiguration: {
paymentTermsTemplateId: companyData.paymentTermsId,
},
},
companyContact: {
customer: {
firstName: companyData.primaryContact.firstName,
lastName: companyData.primaryContact.lastName,
email: companyData.primaryContact.email,
},
},
},
},
},
});

const result = response.body.data.companyCreate;

if (result.userErrors.length > 0) {
throw new Error(
`Failed to create company: ${result.userErrors.map((e) => e.message).join(', ')}`
);
}

return result.company;
}

複数ロケーションの管理

大規模な B2B 顧客は、それぞれ異なる価格設定と支払い条件を持つ複数の配送ロケーションを持つことがよくあります:

export async function addCompanyLocation(client, companyId, locationData) {
const response = await client.query({
data: {
query: `mutation CompanyLocationCreate(
$companyId: ID!,
$input: CompanyLocationInput!
) {
companyLocationCreate(companyId: $companyId, input: $input) {
companyLocation {
id
name
}
userErrors {
field
message
}
}
}`,
variables: {
companyId,
input: {
name: locationData.name,
shippingAddress: locationData.address,
buyerExperienceConfiguration: {
paymentTermsTemplateId: locationData.paymentTermsId,
checkoutToDraft: locationData.requiresApproval,
},
},
},
},
});

return response.body.data.companyLocationCreate;
}
tip

注文にマネージャーの承認が必要な企業ロケーションでは checkoutToDraft: true を設定してください。これにより、そのロケーションからの注文は確定済み注文ではなくドラフト注文として作成され、承認ワークフローが有効になります。

カスタム価格設定とカタログ

カタログは Shopify が B2B 固有の価格設定を管理する方法です。各カタログには、割り当てられた企業ロケーションに対してストアのデフォルト価格を上書きする価格リストが含まれます。

カスタム価格でのカタログ作成

// services/catalog-service.js
export async function createCatalogWithPricing(client, catalogData) {
// Step 1: Create the catalog
const catalogResponse = await client.query({
data: {
query: `mutation CatalogCreate($input: CatalogCreateInput!) {
catalogCreate(input: $input) {
catalog {
id
title
priceList {
id
}
}
userErrors { field message }
}
}`,
variables: {
input: {
title: catalogData.name,
status: 'ACTIVE',
context: {
companyLocationIds: catalogData.locationIds,
},
},
},
},
});

const catalog = catalogResponse.body.data.catalogCreate.catalog;

// Step 2: Add fixed prices to the price list
if (catalogData.fixedPrices?.length > 0) {
await client.query({
data: {
query: `mutation PriceListFixedPricesAdd(
$priceListId: ID!,
$prices: [PriceListPriceInput!]!
) {
priceListFixedPricesAdd(
priceListId: $priceListId,
prices: $prices
) {
prices {
variant { id }
price { amount currencyCode }
}
userErrors { field message }
}
}`,
variables: {
priceListId: catalog.priceList.id,
prices: catalogData.fixedPrices.map((p) => ({
variantId: p.variantId,
price: {
amount: p.price.toString(),
currencyCode: p.currency,
},
compareAtPrice: p.compareAtPrice
? { amount: p.compareAtPrice.toString(), currencyCode: p.currency }
: null,
})),
},
},
});
}

return catalog;
}

パーセンテージベースの価格調整

よりシンプルな卸売割引構造には、固定価格の代わりにパーセンテージ調整を使用します:

export async function setPercentageAdjustment(client, priceListId, percentage) {
const response = await client.query({
data: {
query: `mutation PriceListUpdate($id: ID!, $input: PriceListInput!) {
priceListUpdate(id: $id, input: $input) {
priceList {
id
adjustment {
type
value
}
}
userErrors { field message }
}
}`,
variables: {
id: priceListId,
input: {
adjustment: {
type: 'PERCENTAGE_DECREASE',
value: percentage, // e.g., 20 for 20% off
},
},
},
},
});

return response.body.data.priceListUpdate;
}
warning

カタログ価格は、自動割引や価格ルールを含む他のすべての価格設定メカニズムよりも優先されます。B2B 顧客の価格問題をデバッグする際は、まずカタログの割り当てを確認してください。

発注書承認ワークフロー(Winter '26)

Winter '26 機能

Winter '26 リリースでは、B2B における発注書承認ワークフローのネイティブサポートが導入されました。企業ロケーションに承認ルールを設定し、指定された承認者が注文を確定前にレビューすることを要求できます。これは以前のドラフト注文による回避策に代わるものです。

承認ルールの設定

// services/approval-workflow.js
export async function configureApprovalWorkflow(client, locationId, rules) {
const response = await client.query({
data: {
query: `mutation CompanyLocationApprovalConfig(
$locationId: ID!,
$rules: [ApprovalRuleInput!]!
) {
companyLocationUpdateApprovalRules(
companyLocationId: $locationId,
rules: $rules
) {
companyLocation {
id
approvalRules {
id
condition
threshold { amount currencyCode }
approvers { customer { email } }
}
}
userErrors { field message }
}
}`,
variables: {
locationId,
rules: rules.map((rule) => ({
condition: rule.condition, // 'ORDER_TOTAL_EXCEEDS'
threshold: {
amount: rule.threshold.toString(),
currencyCode: rule.currency,
},
approverContactIds: rule.approverIds,
})),
},
},
});

return response.body.data.companyLocationUpdateApprovalRules;
}

承認ダッシュボードの構築

// routes/app.approvals.jsx
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import {
Page,
Layout,
Card,
DataTable,
Badge,
Button,
ButtonGroup,
} from '@shopify/polaris';

export async function loader({ request }) {
const { admin } = await authenticate.admin(request);

const response = await admin.graphql(`{
draftOrders(first: 50, query: "status:open AND tag:b2b-pending-approval") {
edges {
node {
id
name
createdAt
totalPriceSet {
shopMoney { amount currencyCode }
}
purchasingEntity {
... on PurchasingCompany {
company { name }
location { name }
contact { customer { email } }
}
}
}
}
}
}`);

const { data } = await response.json();
return json({ pendingOrders: data.draftOrders.edges });
}

export default function ApprovalsPage() {
const { pendingOrders } = useLoaderData();

const rows = pendingOrders.map(({ node }) => [
node.name,
node.purchasingEntity?.company?.name || 'N/A',
node.purchasingEntity?.location?.name || 'N/A',
`$${node.totalPriceSet.shopMoney.amount}`,
new Date(node.createdAt).toLocaleDateString(),
<ButtonGroup key={node.id}>
<Button tone="success" onClick={() => approveOrder(node.id)}>
Approve
</Button>
<Button tone="critical" onClick={() => rejectOrder(node.id)}>
Reject
</Button>
</ButtonGroup>,
]);

return (
<Page title="Purchase Order Approvals">
<Layout>
<Layout.Section>
<Card>
<DataTable
columnContentTypes={[
'text', 'text', 'text', 'numeric', 'text', 'text',
]}
headings={[
'Order', 'Company', 'Location', 'Total', 'Date', 'Actions',
]}
rows={rows}
/>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}

B2B チェックアウトカスタマイズ

Shopify Plus マーチャントは Checkout UI Extensions を使用して B2B チェックアウト体験をカスタマイズできます。

PO 番号フィールドの追加

// extensions/checkout-b2b/src/Checkout.jsx
import {
reactExtension,
TextField,
useApplyMetafieldsChange,
useMetafield,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
'purchase.checkout.shipping-option-list.render-after',
() => <PONumberField />
);

function PONumberField() {
const poNumber = useMetafield({
namespace: 'custom',
key: 'po_number',
});

const applyMetafieldsChange = useApplyMetafieldsChange();

const handleChange = (value) => {
applyMetafieldsChange({
type: 'updateMetafield',
namespace: 'custom',
key: 'po_number',
valueType: 'string',
value,
});
};

return (
<TextField
label="Purchase Order Number"
value={poNumber?.value || ''}
onChange={handleChange}
/>
);
}

最小注文数量の適用

// extensions/checkout-b2b/src/MinOrderValidation.jsx
import {
reactExtension,
useBuyerJourneyIntercept,
useCartLines,
Banner,
} from '@shopify/ui-extensions-react/checkout';

export default reactExtension(
'purchase.checkout.block.render',
() => <MinOrderValidation />
);

function MinOrderValidation() {
const cartLines = useCartLines();

const totalQuantity = cartLines.reduce(
(sum, line) => sum + line.quantity,
0
);

const minimumOrder = 24; // Configurable per company

useBuyerJourneyIntercept(({ canBlockProgress }) => {
if (canBlockProgress && totalQuantity < minimumOrder) {
return {
behavior: 'block',
reason: `Minimum order quantity is ${minimumOrder} units`,
errors: [
{
message: `Your cart has ${totalQuantity} items. The minimum order is ${minimumOrder} items.`,
},
],
};
}

return { behavior: 'allow' };
});

if (totalQuantity < minimumOrder) {
return (
<Banner status="warning">
Minimum order is {minimumOrder} units. You currently have{' '}
{totalQuantity}. Please add {minimumOrder - totalQuantity} more items.
</Banner>
);
}

return null;
}

ACH 決済統合

多くの B2B 取引では、クレジットカードではなく ACH(Automated Clearing House)銀行振込が使用されます。Shopify はペイメントアプリと手動決済方法を通じて ACH をサポートしています。

支払い条件による ACH の設定

// services/ach-payment-service.js
export async function configureACHPaymentTerms(client, locationId) {
// First, find or create the ACH payment terms template
const response = await client.query({
data: {
query: `query PaymentTermsTemplates {
paymentTermsTemplates {
id
name
paymentTermsType
dueInDays
}
}`,
},
});

const templates = response.body.data.paymentTermsTemplates;
const net30 = templates.find(
(t) => t.paymentTermsType === 'NET' && t.dueInDays === 30
);

// Assign payment terms to the company location
await client.query({
data: {
query: `mutation AssignPaymentTerms(
$locationId: ID!,
$paymentTermsTemplateId: ID!
) {
companyLocationAssignPaymentTerms(
companyLocationId: $locationId,
paymentTermsTemplateId: $paymentTermsTemplateId
) {
companyLocation {
buyerExperienceConfiguration {
paymentTermsTemplate {
paymentTermsType
dueInDays
}
}
}
userErrors { field message }
}
}`,
variables: {
locationId,
paymentTermsTemplateId: net30.id,
},
},
});
}
danger

B2B の支払い条件(Net 30、Net 60 など)はマーチャントに実際の財務リスクをもたらします。アプリが支払い条件の割り当てを自動化する場合は、新しい企業に条件を適用する前に、与信限度額チェック、支払い履歴の確認、マーチャント承認フローなどのセーフガードを必ず含めてください。

B2B 開発 Checklist

機能複雑さShopify Plus 必須
Company creation and managementMediumYes
Multi-location supportMediumYes
Catalog-based pricingHighYes
Purchase order approvalsHighYes (Winter '26)
Checkout customizationHighYes
Payment terms / ACHMediumYes
B2B customer portalHighYes

Shopify での B2B コマースはまだ成熟途上であり、アプリ開発者にとっての機会は大きいです。レガシーの卸売プラットフォームから移行するマーチャントは、Shopify のネイティブ機能と複雑な運用要件のギャップを埋めるアプリを必要としています。今この分野で専門知識を構築することで、Shopify が B2B 機能に大規模な投資を続ける中で有利なポジションを確保できます。