お問い合わせ
Blog & News
2026.09.14 制作の実務

Squareの決済リンクで、送料をline_itemsに入れてはいけない

[Home] / [Blog] / [Article]

Squareの決済リンクで、送料をline_itemsに入れてはいけない

自社のオンラインショップをSquareで作ったときの話です。

実際の決済ページ・正しい書き方
実際の決済ページ・正しい書き方
実際の決済ページ・間違った書き方
実際の決済ページ・間違った書き方

送料を含めた合計金額は、きちんと合っている。テストの決済も通る。ところが本番の決済ページを開いてみたら、「送料」という項目がどこにもありませんでした

しかも「ご注文内容(2点)」と、買っていないはずの点数まで増えている。

原因は、送料を入れる場所を間違えていたことでした。

何が起きていたか

表示比較の図解
表示比較の図解

やっていたのは、こういうことです。

商品を line_items(明細行)に入れて、そこに送料880円を「1行」として足していました。合計金額は正しく出ます。1,650円の商品なら、2,530円になる。計算は合っている。

ただしSquare側から見ると、明細行が2つある注文になっています。送料は「880円の商品をもう1つ買った」のと同じ扱いです。

実際の決済ページには、こう出ました。

お会計          ¥2,530
ご注文内容(2点)
小計            ¥2,530
注文合計        ¥2,530

送料の行がありません。 小計に飲み込まれています。そして買ったのは1点なのに、2点と表示される。

さらに、住所を入力して配送方法を選ぶところまで進むと、「送料無料」と表示されました。Square側の配送料フィールドは0円のままなので、当然といえば当然です。

お客様の目には、こう映ります。

  • 送料がいくらか書いていない
  • 送料は無料と書いてあるのに、合計は2,530円
  • 注文点数が2点だが、買ったのは1点

金額が合っているだけでは、検証になっていなかったということです。ここは反省しました。

正しい入れ方は checkout_options.shipping_fee

答えを先に書きます。送料は line_items ではなく、checkout_options の中の shipping_fee に入れます。

{
  "idempotency_key": "…",
  "order": {
    "location_id": "…",
    "line_items": [
      { "name": "コースター 4枚セット", "quantity": "1",
        "base_price_money": { "amount": 1650, "currency": "JPY" } }
    ]
  },
  "checkout_options": {
    "shipping_fee": {
      "name": "送料(茨城県)",
      "charge": { "amount": 880, "currency": "JPY" }
    },
    "ask_for_shipping_address": true
  }
}

こう書くと、送料は「service charge(SUBTOTAL_PHASE)」として計上されます。同じ内容で決済リンクを作り直すと、表示がこうなりました。

コースター 4枚セット   ¥2,530
ご注文内容(1点)
小計                 ¥1,650
送料(茨城県)        ¥880
注文合計             ¥2,530

送料が独立した行として出ます。 点数も1点に戻りました。商品名も表示されるようになっています。

同じ2,530円でも、見え方はまったく別物です。

PHPで書くと、こんな形になります。実際に動いているものを、そのまま短くしたものです。

<?php
function create_payment_link(array $items, int $shippingFee, string $shippingName): array
{
    $lineItems = [];
    foreach ($items as $it) {
        $lineItems[] = [
            'name'             => $it['name'],
            'quantity'         => (string)$it['qty'],
            'base_price_money' => ['amount' => $it['price'], 'currency' => 'JPY'],
        ];
    }

    $body = [
        'idempotency_key' => bin2hex(random_bytes(16)),
        'order' => [
            'location_id' => SQUARE_LOCATION_ID,
            'line_items'  => $lineItems,
        ],
        'checkout_options' => [
            'ask_for_shipping_address' => true,
        ],
    ];

    // 送料は line_items ではなく checkout_options に入れる
    if ($shippingFee > 0) {
        $body['checkout_options']['shipping_fee'] = [
            'name'   => $shippingName,
            'charge' => ['amount' => $shippingFee, 'currency' => 'JPY'],
        ];
    }

    $ch = curl_init('https://connect.squareup.com/v2/online-checkout/payment-links');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Square-Version: 2026-08-20',
            'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($body, JSON_UNESCAPED_UNICODE),
        CURLOPT_TIMEOUT        => 20,
    ]);
    $res  = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 200) {
        // 決済リンクが作れなかったときは、金額を表示したまま止める
        throw new RuntimeException('Square payment link error: ' . $res);
    }
    return json_decode($res, true)['payment_link'];
}

ポイントは2つだけです。

shipping_feecheckout_options の中に置くこと。そして Square-Version ヘッダーでAPIのバージョンを固定すること。バージョンを指定しないとアカウントの既定値が使われるので、ある日いきなり挙動が変わることがあります。日付を書いておくと、そこで固定されます。

idempotency_key は毎回ランダムで問題ありません。同じキーを再送すると同じリンクが返る仕組みなので、通信エラーで二重に作られるのを防いでくれます。

JPYはゼロ十進通貨、という落とし穴

もうひとつ、最初に踏みかけたところです。

amount に入れる数字は、円そのままでいいです。330と書けば330円になります。

海外のサンプルコードを見ていると、USDで「$3.30 なら 330」と書いてあります。これはドルがセント単位、つまり100分の1まで扱う通貨だからです。

日本円は「ゼロ十進通貨」といって、その下の単位がありません。なので円をそのまま書くのが正解です。ここで100倍して書いてしまうと、330円のつもりが33,000円の請求になります。

サンプルをそのまま持ってくると事故る場所なので、最初に確認しておくと安心です。

Sandboxでは気づけない

ここがいちばん伝えたいところかもしれません。

Squareには開発用のSandbox環境があります。ところがSandboxで作った決済リンクを開くと、Squareの「Testing Panel」に飛びます。実際の決済ページのUIではありません。

つまり、

  • 送料の行がどう見えるか
  • 注文点数がどう出るか
  • 3Dセキュアの画面がどう挟まるか

これらは本番でしか確認できません。私が送料の行が出ていないことに気づいたのも、本番で決済リンクを作ってからでした。

では本番で試すと在庫や売上が汚れるのか、というと、そこは手順で避けられます。

  1. CreatePaymentLink でリンクを作る
  2. リンクを開いて、実際の決済ページを目で見る
  3. GET /v2/orders/{orderId} で金額の内訳を確認する
  4. DeletePaymentLink で即削除する

決済を完了させなければ在庫は動きません。リンクも消えます。この4ステップなら、本番環境でも安全に確認できます。

「実際の決済ページを目で見る」を工程に入れておくのが、いちばん確実だと思います。

地域別の送料は、静的なリンクでは扱えない

都道府県によって送料が変わる設計にする場合、Squareの管理画面で作る「固定の決済リンク」では対応できません。

shipping_fee は注文ごとに値が変わるので、注文のたびに CreatePaymentLink でリンクを生成する必要があります。

つまりこうなります。

  • サイト側で送り先の地域を選んでもらう
  • 地域から送料を計算する
  • その金額を入れて決済リンクを作る
  • そのリンクへお客様を飛ばす

ここまで来ると、「Squareの管理画面でリンクを作って貼る」という運用では足りなくなります。PHPなりで中継する仕組みが必要です。

逆に言うと、全国一律送料にできるなら、実装はかなり楽になります。ここは設計の最初に決めておくのがいいと思います。

それでもSquareを選ぶ理由

書いてきたことだけ読むと、面倒な印象を持たれるかもしれません。それでもSquareを選んだ理由があります。

商品と在庫と価格を、1か所にまとめられるからです。

うちはイベント出店を見据えてSquareに登録しました(まだ出店はできていません)。対面で売るならSquareの端末を使うことになる。そのとき、サイト側とイベント側で在庫を別々に管理したくなかった

なので、Squareを在庫と価格の「正」にして、サイト側は表示だけを担当する構成にしました。商品を追加するときも、価格を変えるときも、触るのはSquareだけです。サイトは何もしなくていい。

すでに実店舗でSquareをお使いの事業者さんなら、同じ考え方がそのまま使えると思います。商品も在庫もすでにSquareの中にあるので、ECを足すときの手間がいちばん小さくなります。

それに、日本のオンラインカード決済は3Dセキュアが必須で、加盟店側で無効にできません。自前で決済フォームを作ると、本人認証の実装まで自分でやることになります。

ホスト型の決済リンクを使えば、3DSもカード情報の取り扱いもSquare側で完結します。ここは正直、かなり楽です。

まとめ

  • 送料は line_items ではなく checkout_options.shipping_fee に入れる
  • 日本円はそのままの数字。100倍しない
  • Sandboxでは決済ページの見た目を確認できない。本番で作って、目で見て、消す
  • 地域別送料にするなら、決済リンクは注文ごとに動的生成する

金額が合っていても、画面が間違っていることがあります。ECは1円のずれが信用に直結する場所なので、最後は必ず自分の目で見る。これに尽きるなと思いました。

同じところで詰まっている方の役に立てば幸いです。