Customer Account UI Extensionsから外部APIを呼ぶ:network_access・CORS・実データでの検証手法【実践チュートリアル】

2026年8月、Shopify CLI 3.90、api_version 2026-04、New Customer Accountsを有効にした認可済みテスト環境で検証しています。本文では、公式ドキュメントに記載のある仕様(リンク付き)と、公式に記載がなく2026年8月時点の検証で確かめた挙動を区別して書く方針です。

はじめに:外部APIを呼ぶ前に確かめること

マイページに、Shopifyの外にあるシステムのデータを出したい場面があります。たとえば、会員ランクやポイント残高、外部で管理している手続きの進み具合を表示する場面です。Customer Account UI Extensions(以下UIE)はextensionから外部APIを直接呼べるので、こうした表示は仕組みのうえでは作れます(capabilities)。

外部システムから表示したいデータをメタフィールドに置けないか、実装に着手する前に確かめます。公式のnetwork accessの解説自体が先にメタフィールドでの代替を検討するよう案内しているのは、事前に書き込めるデータなら外部への呼び出しは要らず、表示の速度と信頼性をShopify側に任せられるからです。メタフィールドの読み書きはCustomer Account UI Extensionsで過去注文のデータを読む・書く:signal・GraphQL・メタフィールドの使い分けで扱っています。

それでも外部APIが必要になるのは、リアルタイムの判定が要るときと、データの正本が外部システムにあって同期しきれないときの2つです。このどちらかに当たると分かったら、次に決めるのは認証の扱いで、UIEだからといってバックエンドが必須になるわけではありません。APIキーのような秘匿情報を保管する必要があればバックエンドを置き、認証のない公開APIが適切なCORSヘッダを返しているなら、UIEから直接fetchできます。後者は公式の仕様どおりで、検証ストアでも同じ挙動を確認しました。すでにブラウザから呼ばれている認証なしAPIのURLは公開情報なので、それをバックエンドで包み直してもセキュリティは上がりません。

1. 仕組みを理解する:null originとAccess-Control-Allow-Origin

UIEはsandboxed Web Workerの中で動き、ページのJavaScriptからは隔離されています。この環境から発するリクエストには認識できるオリジンが乗らず、いわゆるnull originです。外部APIを呼ぶための要件が2つあるのはこのためで、公式はextension側とサーバー側に一つずつ挙げています。

extension側の要件は、shopify.extension.toml[extensions.capabilities]network_accessを宣言することです。

[extensions.capabilities]
network_access = true

サーバー側の要件は、レスポンスに次のヘッダを返すことです。

Access-Control-Allow-Origin: *

特定のオリジンだけを許可する書き方では足りません。null originには照合できる値がなく、ワイルドカードの*でなければ受け付けられないためです。公式の記載どおりで、検証ストアでも同じ挙動でした。

*を返すことと、APIを無防備にすることは別です。公開してよい情報だけを返す、入力を検証する、レート制限をかける、といったAPI自体の防御は、このヘッダを付けても付けなくても変わらず必要なまま。ブラウザはこのヘッダを見て、JavaScriptへレスポンスを渡すかどうかを決めます。サーバーが返す情報の範囲は、API側で別に制御します。

network_accessの宣言と許可:tomlとPartner Dashboardの設定

tomlの宣言に加えて、Partner Dashboardのアプリ設定で「Allow network access in checkout and account UI extensions」を有効にします。公式ドキュメントによれば、この許可は自動で承認され、必要なapproval scopeが即座に付与されます。

2026年8月の検証ストアでは、新しいDev Dashboardに旧ドキュメントにあった申請のUIが見当たらず、tomlに宣言した状態でdeployがそのまま通りました。自動承認が公式の記載で、申請UIが見当たらないこととdeployが通ったことはその時点の画面と操作の実測、という関係です。

呼び出し自体は通常のfetchです。次のコードは、表示中の商品に外部の会員システムの特典を使えるかを判定APIに問い合わせ、結果を3つの状態で表示する最小例です。エンドポイントのURLはコードに直書きせず、extensionの設定からshopify.settings.value.benefit_endpointとして受け取ります。設定には秘匿情報を置かない前提です。

import '@shopify/ui-extensions/preact';
import {render} from 'preact';
import {useEffect, useState} from 'preact/hooks';

export default async () => {
  render(<BenefitAvailability />, document.body);
};

function BenefitAvailability() {
  const [state, setState] = useState({status: 'loading'});

  useEffect(() => {
    const controller = new AbortController();
    const endpoint = shopify.settings.value.benefit_endpoint;
    const productId = shopify.target.value.merchandise.product.id;

    async function load() {
      try {
        const url = new URL(endpoint);
        url.searchParams.set('product_id', productId);

        const response = await fetch(url, {signal: controller.signal});
        if (!response.ok) {
          throw new Error(`Benefit API returned ${response.status}`);
        }

        const data = await response.json();
        setState({status: 'ready', available: data.available === true});
      } catch (error) {
        if (error.name !== 'AbortError') {
          setState({status: 'error'});
        }
      }
    }

    load();
    return () => controller.abort();
  }, []);

  if (state.status === 'loading') {
    return <s-text>特典を使えるか確認しています</s-text>;
  }
  if (state.status === 'error') {
    return <s-text>特典を使えるかどうかを確認できませんでした。</s-text>;
  }
  return state.available
    ? <s-text>この商品に特典を使えます。</s-text>
    : <s-text>この商品には特典を使えません。</s-text>;
}

コンポーネントの破棄時にはAbortControllerでリクエストを中断し、中断によるAbortErrorerror状態に移しません。商品IDはshopify.target.value.merchandise.product.idから取り、product_idクエリに載せる形です。レスポンスはresponse.okが成り立たなければBenefit API returned ${response.status}をthrowしてerrorに落とし、成り立てばdata.available === trueで判定します。状態はloadingreadyerrorの3つで、この3状態の扱いは4節で設計の話として戻ってきます。

fetchが成功しても、返ってきたJSONをそのまま信じてよいわけではありません。外部APIを呼ぶ場合は、HTTPステータスを確認し、必要なフィールドを検証したうえで、通信に失敗したときのUIも用意します。

2. テーマから呼んでいた外部APIをUIEから呼ぶ:中継バックエンドの要否

テーマからの移行でよくあるのが、テーマのJavaScriptから呼んでいた外部APIを、UIEからも同じように呼びたいという状況です。事前の設計では、サンドボックスの中からではCORSで弾かれるだろうと考えて、バックエンドで中継する構成を想定しがちでした。

2026年8月の検証では、テーマ(storefront)から呼べていたAPIをUIEからも直接呼び出せました。そのサーバーがAccess-Control-Allow-Origin: *を返す設定だったからで、外部APIの中継だけを目的にしたバックエンドは要らなくなりました。テーマからクロスオリジンで呼べていたAPIなら、レスポンスヘッダを確認します。Access-Control-Allow-Origin: *が返っていれば、UIEから直接呼ぶためのサーバー側の要件を満たしています。

ただし、同じ構成にできないAPIもあります。特定のオリジンだけを許可しているAPIや、Cookieや秘匿の認証に依存するAPIは、前節の要件を満たせません。既存の実装を見ただけで決めず、次の4段で確かめます。

  1. 外部APIに秘匿の認証情報が必要かを確認します。必要ならバックエンドで中継します。
  2. APIのレスポンスがAccess-Control-Allow-Origin: *を返しているかを確認します。
  3. tomlにnetwork_access = trueを宣言して、deployします。
  4. 実際のUIEで「成功」「業務エラー」「通信失敗」の3パターンを実機で確認します。

3. CORSエラーと403の切り分け:どこで止まっているかの見方

外部通信のデバッグでは、CORSエラーと403を分けて調べます。画面上ではどちらもAPI呼び出しの失敗に見えますが、リクエストが止まった場所は別です。

見え方 意味 次の一手
CORSエラー(ブラウザがブロック) レスポンスに必要なヘッダが無く、ブラウザがJavaScriptに結果を渡していない サーバー側のAccess-Control-Allow-Originを確認(必要ならpreflightへの応答も)
HTTP 403 リクエストはどこかのサーバー層に到達したうえで拒否 エンドポイント単位の認可、IP制限、WAFなどアクセス制御を確認

2026年8月の検証では、同一ドメインのあるAPIは通るのに、別のエンドポイントだけが403を返す場面がありました。最初はCORSの問題と見て、サーバーのCORS設定を見直し続けましたが、解決しませんでした。403がJavaScriptから見えているということは、少なくともリクエストはサーバー側に届いています。届いたうえで拒否されているのだから、疑う先はドメイン全体の設定ではなくエンドポイント単位の認可で、403を返しているのがCDNやWAFの層である可能性も含めて確認します。

切り分けに入る前提として、そもそもextensionが描画されているかの確認が先です。extension全体が何も表示されない場合、通信より手前のランタイムエラーが原因のことがあり、その見方はCustomer Account UI Extensions開発の始め方:scaffoldから実ストアでの検証までで扱っています。

4. 外部依存UIの設計:判定が取れなかったときの操作の扱い

外部APIの判定結果で操作を切り替えるUIでは、判定を取得できなかった場合の挙動も決めておく必要があります。外部の会員システムに特典を使えるかを問い合わせ、結果によって注文の操作を切り替えるUIを例に、方針を3つ比べます。

判定できなくても操作を許すfail-openは、本来は使えない特典が適用されたまま注文が通るリスクがあります。判定できなければ画面の操作をすべて止めるfail-closedは、外部が応答しないだけで、特典と関係のない操作まで使えなくなります。

検証で採ったのは、外部判定に依存する操作だけを無効にする方針です。外部判定と関係しない操作は、通信に失敗しても残します。

操作 外部判定への依存 通信失敗時
特典を使って注文 依存する 無効化し、確認できない旨を表示
特典を使わずに注文 依存しない 利用可能なまま

状態は最小例と同じくloadingreadyerrorの3つに分け、読み込み中に押せるボタンを一瞬でも表示しません。ready前にボタンが押せると、判定が届く前の操作を許すことになるためです。判定結果が不可なのか、通信に失敗したのかは利用者にとって意味が違うので、エラーを「特典は使えない」と偽装することもせず、判定を確認できなかった状態として扱います。安全側の動作を選んでも、外部判定に依存しない機能まで使えなくする必要はありません。

5. 実データでどう検証するか

UIEの開発では、リファレンスの記載がそのまま動くとは限らないことを、検証の中で経験しました。この経験から、スキーマで確認したあとに実機で確かめる手順を取りました。この手順は、AIエージェントへ渡す指示にも含められます。

スキーマ検証と実機検証の2段構え

実装の前に、GraphQLは必ずスキーマバリデーションを通します。存在しないフィールドの検出と、公式リファレンスでの権限要件の確認は、この段階で済ませておく作業です。Customer Account APIのOrderにcustomAttributesが存在しない、という事実はここで分かります。公式の型定義どおりで、検証ストアでも同じでした。この件はCustomer Account UI Extensionsで過去注文のデータを読む・書く:signal・GraphQL・メタフィールドの使い分けで詳しく扱っています。

実装の後には実機で確かめます。スキーマにあるフィールドでも、そのtargetのランタイムで値が返るとは限らないためです。検証では型定義にあるフィールドがsignalに入らなかった例があり、最終判断は実機に置きました。2段に分けるのは、リファレンスを信じて設計したものが実機で動かない、という事故を防ぐため。

テストデータの作り方:draftOrderを使った3段の手順

検証には特定の形の注文が要ります。複数数量の商品、一部キャンセル扱いの明細、プロパティのない注文など。管理画面で偶然そうなるのを待つより、Admin APIで狙った形に合成する方が速く、同じ状態を再現できます。下書き注文を作成して注文化し、表示検証に必要な属性を注文へ追加します。

  1. draftOrderCreateで下書き注文を作成します。商品、数量、ラインアイテムプロパティはここで指定します。
  2. draftOrderCompleteで注文にします。
  3. orderUpdateで注文のcustomAttributesを後付けします。キャンセル扱いや外部システム側のステータスといった状態を表示検証用の属性として再現するためで、実際のキャンセル処理は行いません。

注文を直接作るorderCreateを使わないのは、権限の制約で使えない環境が多いためです。下書き注文を経由する方法でも、価格書き換え系のアプリが入ったストアでは、completeが「提示価格が無効」で弾かれ続けることがありました。draftにpriceOverrideで価格を固定してからcompleteすると通り、2026年8月に検証ストアで確認しました。

テストデータを作る過程では、環境による在庫設定の違いも表に出ます。テストストアと本番で、在庫追跡の有無や在庫切れ時の販売可否が異なることは珍しくなく、テストデータ作りはこうした環境差に気づく機会にもなります。

テストケースは1件に詰め込みません。「数量2の商品」「一部キャンセル」「ステータスの有無」「プロパティの有無」のように、期待する結果が一意に決まる小さな注文へ分けます。1件に詰め込むと、表示が崩れたときにどの条件が効いたのかを切り分けられなくなるためです。注文の作成と更新は、認可されたテスト環境だけで行います。

分岐UIの出し方:データ側の可逆な変更

「在庫切れ」「特典の利用不可」「判定不能」のような分岐UIを画面に出すには、コードにデバッグフラグを仕込む方法と、データ側を一時的に変える方法があります。本番と同じコードパスを通すには、データ側で分岐条件を作る方法が適しています。商品メタフィールドの値を変える、バリアントの在庫ポリシーを変える、といった操作で、分岐条件そのものを作ります。

データを変更する前に、変更前の値、対象リソース、復元手順を記録します。検証後は、その記録に沿って元の値へ戻します。

例外は通信失敗の再現で、外部APIを勝手に止める方法は取れません。テスト用の失敗条件を用意し、他の利用者に影響しない範囲で試します。

AIに開発させるときの指示:検証プロトコルの渡し方

「外部APIを呼ぶUIEを作って」とだけ指示すると、コードを生成したところで作業が終わってしまうため、この記事の検証手法をそのまま指示に含めます。

事前確認:
  network_access 宣言 / 外部 API の認証方式 / CORS 応答 / 必要 scope
テストケース:
  成功 / 業務上の不可 / 403 / CORS エラー / タイムアウト / 不正な JSON / 空値
期待 UI:
  loading・ready・error ごとの表示と、操作ごとの有効・無効
データ操作:
  対象 / 変更前後の値 / 可逆性 / 復元手順 / 接続先ストアの確認
完了条件:
  スキーマ検証済み / 実機確認済み / ログに機密なし / データ復元済み

完了条件には、実機確認、公式仕様と実測結果を分けた報告、分岐確認後のデータ復元を含めます。指示に実機確認を含めておけば、リファレンスの記載だけを根拠に実装を終える事態を避けられます。実ストアで描画と動作を確かめるところまでを完了条件にする理由は、Customer Account UI Extensions開発の始め方:scaffoldから実ストアでの検証までで扱っている内容です。

シリーズのまとめ:3つの判断フレーム

バックエンドの要否は、外部依存の認証方針で決まります。表示、カート投入、データの引き継ぎ、顧客入力の保存までは、UIE単体で完結する範囲です。バックエンドを検討するのは、秘匿情報の置き場所が要るとき、Webhookを起点に注文へ自動で書き込むとき、レガシーデータを書き戻すときの3つ。

データアクセスは、signal、GraphQL、メタフィールドの三層で捉えます。どの層で何が取れるかはリファレンスだけでは設計できず、実機との突き合わせを工程に組み込みます。

storefrontへの遷移にはhrefしか使えません。リンクが押せる時点でURLが確定している前提で、事前構築を軸に設計します。公式の仕様で、検証ストアでも同じでした。

できること、できないことの早見表とユースケース集は、Shopify「新しいお客様アカウント」のマイページに独自機能を組み込む:UI Extensionsでできること・できないことにまとめています。

FAQ

Q:外部APIの認証トークンをextensionに埋め込んでもいいですか?

埋め込めません。extensionのコードはブラウザに配布されるので、埋め込んだ秘匿情報は公開したのと同じになります。認証が必要なAPIを呼ぶ場合が、まさにバックエンドを立てる条件です。

Q:403が返ってきたらCORSの設定ミスですか?

別の問題として調べます。403がJavaScriptから見えている時点で、リクエストはサーバー層に到達しています。確認先はエンドポイントの認可条件、IP制限、WAFです。CORSエラーの場合は、サーバーのレスポンスヘッダから確認します。

Q:メタフィールドと外部API、どちらを選ぶべきですか?

Shopify内に正本を置ける表示データならメタフィールドです。読み取りにはtomlの宣言とaccess設定が必要で、手順はCustomer Account UI Extensionsで過去注文のデータを読む・書く:signal・GraphQL・メタフィールドの使い分けにあります。外部システムの最新状態が正本である判定なら外部APIで、認証が必要ならバックエンドで中継します。

参考にした公式ドキュメント

WRITTEN BY

キツネ

Shopify Developer

デザインとエンジニアリングの両輪で、Shopifyのストア構築・技術支援に取り組んでいます。2019年頃からShopifyでのEC制作を続け、これまで累計100件以上のストアに携わってきました。既存のアプリだけでは実現が難しい独自要件を、設計と開発で形にすることを大切にしています。現場で得た知見を発信しています。

話を聞いてみる
BACK TO TECH INSIGHTS