2026年9月24日

2026年9月24日

WooCommerceで決済完了後に外部APIを呼び出す実装方法(フック選びと再送設計)

WooCommerceで決済が完了した注文を外部の基幹システムや配送サービスのAPIへ送るなら、woocommerce_payment_complete か注文ステータス変更のフックで注文IDを受け取り、API呼び出し自体は Action Scheduler の非同期ジョブに回すのが安全です。この記事では、どのフックを選ぶか、二重送信と送信漏れをどう防ぐか、失敗時にどう再送しログを残すかを、そのまま使えるコードとあわせて解説します。

先に決めておくこと:「決済完了」をどう定義するか

実装に入る前に、外部APIへ送るタイミングを「どの状態になった注文か」で決めておきます。WooCommerceの注文は決済方法によって経路が違い、同じ「決済完了」でも発火するフックが変わるためです。

  • オンライン決済(クレジットカードなど):決済ゲートウェイが $order->payment_complete() を呼び、woocommerce_payment_complete アクションが発火します。ステータスは通常「処理中」になり、ダウンロード商品だけなら「完了」になることもあります。
  • 銀行振込・代金引換などのオフライン決済:多くの場合 payment_complete() を通らず、ステータスが「保留中」や「処理中」に直接変わります。woocommerce_payment_complete だけを使うと、こうした注文は送信されません。
  • **サンクスページ(woocommerce_thankyou)**:購入者がサンクスページを表示したときだけ発火します。ブラウザを閉じた、決済サービスから戻らなかったといった理由で発火しないことがあるため、業務データの送信には向きません。

迷ったら、「processing(処理中)か completed(完了)になった時点で一度だけ送る」と決め、ステータス変更フックで拾う方法が扱いやすいです。オンライン決済だけを対象にするなら woocommerce_payment_complete でも構いません。

なぜその場で wp_remote_post() を呼ばないのか

フックの中で直接 wp_remote_post() を呼ぶと、外部APIの応答が遅いだけで購入者の決済画面や決済ゲートウェイからの通知処理が待たされます。タイムアウトすれば送信は失われ、決済ゲートウェイによっては通知の再送が繰り返されて二重送信の原因にもなります。

そこで、フックでは「この注文を送る」という予約だけを入れ、実際の送信はWooCommerceに同梱されている(2026年10月現在)Action Scheduler に任せます。Action Scheduler は予約した処理をバックグラウンドで実行し、成功・失敗の履歴を管理画面(WooCommerce の「ステータス」内にある「予定されたアクション」。表記は版により異なります)で確認できます。

実装コード

ここでは独立したプラグインとして作ります。テーマの functions.php に書くとテーマ変更で止まるため、wp-content/plugins/my-order-sync/my-order-sync.php などに置くか、mu-plugin にするのがおすすめです。作業前にはサイトとデータベースのバックアップを取り、ステージング環境で試してください。

1. 送信の予約(フック側)

<?php
/**
 * Plugin Name: My Order Sync
 * Description: 決済完了した注文を外部APIへ送信する。
 */

defined( 'ABSPATH' ) || exit;

// HPOS(高性能注文ストレージ)対応を宣言する。
add_action( 'before_woocommerce_init', function () {
	if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
	}
} );

/**
 * 注文が処理中・完了になったら送信ジョブを予約する。
 */
function my_order_sync_maybe_queue( $order_id, $order ) {
	if ( ! $order instanceof WC_Order ) {
		$order = wc_get_order( $order_id );
	}
	if ( ! $order ) {
		return;
	}

	// すでに送信済み、または予約済みなら何もしない(二重送信の防止)。
	if ( $order->get_meta( '_my_sync_status' ) ) {
		return;
	}

	$order->update_meta_data( '_my_sync_status', 'queued' );
	$order->save();

	as_enqueue_async_action( 'my_order_sync_send', array( $order->get_id(), 1 ), 'my-order-sync' );
}
add_action( 'woocommerce_order_status_processing', 'my_order_sync_maybe_queue', 10, 2 );
add_action( 'woocommerce_order_status_completed', 'my_order_sync_maybe_queue', 10, 2 );

woocommerce_order_status_{ステータス} には注文IDと注文オブジェクトの2つが渡されます。注文の読み書きは wc_get_order() と get_meta() / update_meta_data() を使い、get_post_meta() は使いません。HPOSが有効なストアでは注文が投稿テーブルに無いため、投稿メタ関数では値が取れないからです。

2. 送信処理(ジョブ側)

/**
 * Action Scheduler から呼ばれる送信処理。
 */
function my_order_sync_send( $order_id, $attempt = 1 ) {
	$order = wc_get_order( $order_id );
	if ( ! $order || 'sent' === $order->get_meta( '_my_sync_status' ) ) {
		return;
	}

	$items = array();
	foreach ( $order->get_items() as $item ) {
		$product = $item->get_product();
		$items[] = array(
			'sku'      => $product ? $product->get_sku() : '',
			'quantity' => $item->get_quantity(),
			'total'    => $item->get_total(),
		);
	}

	$payload = array(
		'order_number' => $order->get_order_number(),
		'total'        => $order->get_total(),
		'currency'     => $order->get_currency(),
		'items'        => $items,
	);

	$response = wp_remote_post(
		MY_SYNC_API_URL, // wp-config.php で定義する。
		array(
			'timeout' => 15,
			'headers' => array(
				'Content-Type'    => 'application/json',
				'Authorization'   => 'Bearer ' . MY_SYNC_API_KEY,
				'Idempotency-Key' => 'order-' . $order->get_id(), // API側が対応していれば重複を弾ける。
			),
			'body'    => wp_json_encode( $payload ),
		)
	);

	$logger  = wc_get_logger();
	$context = array( 'source' => 'my-order-sync' );
	$code    = is_wp_error( $response ) ? 0 : wp_remote_retrieve_response_code( $response );

	if ( $code >= 200 && $code < 300 ) {
		$order->update_meta_data( '_my_sync_status', 'sent' );
		$order->save();
		$order->add_order_note( '外部APIへ送信しました。' );
		return;
	}

	$reason = is_wp_error( $response ) ? $response->get_error_message() : 'HTTP ' . $code;
	$logger->warning( sprintf( 'order %d attempt %d failed: %s', $order->get_id(), $attempt, $reason ), $context );

	// 4xx はリクエスト内容の誤りなので再送しない。5xx と通信エラーだけ間隔を空けて再送する。
	if ( ( 0 === $code || $code >= 500 ) && $attempt < 5 ) {
		as_schedule_single_action(
			time() + 300 * $attempt,
			'my_order_sync_send',
			array( $order->get_id(), $attempt + 1 ),
			'my-order-sync'
		);
		return;
	}

	$order->update_meta_data( '_my_sync_status', 'failed' );
	$order->save();
	$order->add_order_note( '外部APIへの送信に失敗しました(' . $reason . ')。' );
}
add_action( 'my_order_sync_send', 'my_order_sync_send', 10, 2 );

Action Scheduler には引数を「注文ID、試行回数」の順の配列で渡しています。APIのURLとキーは wp-config.php に定数で置きます(define( 'MY_SYNC_API_URL', 'https://api.example.com/orders' ); と define( 'MY_SYNC_API_KEY', 'your-api-key' );)。コードやデータベースの設定値に直接書くと、バックアップやGitから漏れやすくなります。

つまずきやすいポイント

二重送信:ステータスは「処理中→完了」と続けて変わることがあり、両方のフックで予約すると2回送られます。上のコードは注文メタ _my_sync_status で予約済みかを見て止めています。API側が冪等キー(Idempotency-Key など)に対応していれば、それも併用すると安心です。

送信内容の個人情報:氏名・住所・電話番号を送るなら、送信先が業務上必要な相手か、プライバシーポリシーに第三者提供や委託先として記載があるかを確認してください。ログには注文番号と失敗理由だけを残し、個人情報やAPIキーは書き出さないようにします。

Action Scheduler が動かない:ジョブは WP-Cron やアクセスを契機に実行されます。アクセスが少ないサイトや DISABLE_WP_CRON を有効にしているサイトでは、サーバーのcronから wp-cron.php を定期実行する設定が必要です。予定されたアクションの一覧で「保留中」のまま溜まっていれば、まずここを疑います。

キャンセル・返金:決済後のキャンセルや返金も外部へ伝えるなら、woocommerce_order_status_cancelled や woocommerce_order_status_refunded で別のジョブを予約します。送信済みでない注文のキャンセルは送らない、といった分岐も決めておきます。

確認方法

ステージング環境で、決済サービスのテストモード(サンドボックス)を使って注文を作ります。確認するのは次の3点です。

  1. 注文の「注文メモ」に送信成功のメモが付くこと。
  2. WooCommerce の「ステータス」→「ログ」で、ソース my-order-sync に失敗ログが出ていないこと(ログ画面の表記は版により異なります)。
  3. 送信先URLをわざと存在しないものに変え、再送が予約されて最後に失敗メモが付くこと。

外部APIの代わりに、受信内容を表示するだけのテスト用エンドポイントを用意すると、送られたJSONを目で確かめられて便利です。

コードを書かずに済む場合:WooCommerce標準のWebhook

送信先が受け取るJSONの形を自由に決められるなら、WooCommerce標準のWebhook機能(設定の「高度な設定」内にあります。画面の表記は版により異なります)で「注文の更新」などのトピックを選ぶ方法もあります。署名用のシークレットが付き、送信履歴も残ります。ただし送られる内容はWooCommerceの注文データの形式そのままで、ステータスの絞り込みや項目の加工はできません。送信先の仕様が決まっている場合は、この記事のように自前で組み立てるほうが合わせやすいです。

まとめ

  • 送信のきっかけは woocommerce_payment_complete か注文ステータス変更フック。サンクスページのフックは業務データには使わない。
  • オフライン決済も対象にするなら、processing / completed への変更で拾う。
  • API呼び出しは Action Scheduler で非同期にし、注文メタで二重送信を防ぎ、5xx と通信エラーだけ間隔を空けて再送する。
  • 注文の読み書きは wc_get_order() と注文オブジェクトのメソッドで行い、HPOS対応を宣言する。
  • APIキーは wp-config.php の定数に置き、ログに個人情報を残さない。テストは決済サービスのテストモードとステージングで行う。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲