この記事の範囲と前提

以前、オリジナルの Android アプリから Square Terminal 端末と連携して会計処理をする仕事に携わったときの記録です。公式ドキュメントが分散しており目当ての情報に辿り着くまで苦労したので、プロジェクトの備忘録として大まかな手順をまとめました。

この記事では次のことを扱います。

  • Square Terminal 端末をアプリ側のカードリーダーとして使う前提
  • Terminal API でデバイスコードを発行し、端末とペアリングするまで
  • アプリ側からチェックアウト(金額表示)リクエストを送り、状態を確認・キャンセルするまで
  • ACCESS_TOKEN、device_ididempotency_key といった必須の値の扱い
  • Webhook の利用シーンと最低限の運用注意

前提として次が必要です。

  • Square Developer アカウントと、そこで作成した Application 一式(ACCESS_TOKEN・location_id 取得用)
  • 本番アカウントでサインインできる Square Terminal 端末 1 台以上
  • 端末を叩く側のアプリケーション(この記事では Android アプリを想定していますが、HTTP が投げられれば言語は問いません)
  • curl または Square API Explorer など、API を叩ける環境

サンプルコードでは Square-Version として 2024-06-042024-04-17 の両方が出てきます。これは当時実際に動作確認したバージョンで、現行の API バージョンに合わせて読み替えてください。Square API は後方互換を保ったままバージョン番号が定期的に上がっていく仕組みです。

この記事の内容に関する個別の質問にはお答えできません。あくまで「当時こうやって動かしたメモ」として参照してください。

公式ドキュメントとツール

実装中によく参照したものは次の通りです。手元から開きやすいよう、最初にまとめておきます。

関連する記事

Android アプリ側の作りや、ACCESS_TOKEN を扱う中間サーバーの作り方は別記事にまとめています。Square 連携を組み立てる前後で参考になりそうなものを並べておきます。

会計処理のおおまかな流れ

実装してみると、流れ自体は単純です。

  1. Terminal API でデバイスコードを発行する(再発行しない限り一度だけ)
  2. Square Terminal 端末へ、そのデバイスコードでログインしてペアリングする
  3. アプリ側から、請求金額と device_id を指定して Terminal API のチェックアウトを叩く
  4. お客様が Square Terminal でカードや電子マネーで支払う
  5. チェックアウト ID(セッション ID 相当)を元にステータスを確認し、アプリ側で完了・失敗を分岐する

以降の節は、この 1〜5 を順に追っていく構成になっています。

Square Terminal 端末へデバイスコードでログイン

Square Terminal 端末は、デフォルトで商品閲覧や POS 機能が提供されています。今回のプロジェクトでは商品管理や POS 機能を自社側で開発するため、Square Terminal 端末を カードリーダーとしてだけ 使いたかったのですが、このやり方に辿り着くのに苦労しました😅

ポイントは、Square Terminal 端末にログインする際に、メールアドレスなどのアカウントではなく、Terminal API で発行したデバイスコードでログインする ことです。これにより Square Terminal 端末がカードリーダーモードになります。すでにアカウントでログインしてしまっている場合は、一旦ログアウトしてからやり直します。

デバイスコード発行 API

デバイスコードを発行するには、以下の API を叩きます。location_id は Square Dashboard の Locations から確認できます。

curl https://connect.squareup.com/v2/devices/codes \
  -X POST \
  -H 'Square-Version: 2024-06-04' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "idempotency_key": "XXXXXXXXXXXXXXXX",
    "device_code": {
      "name": "Counter 1",
      "product_type": "TERMINAL_API",
      "location_id": "XXXXXXXXXXXXXXXX"
    }
  }'

手元から試すだけなら、Square API Explorer の CreateDeviceCode からブラウザ越しに叩くのが一番手軽です。

ACCESS_TOKEN の扱い

API を叩く際には ACCESS_TOKEN が必要になります。ACCESS_TOKEN は Square Developer の管理画面で Application を作成すると発行されます。

ACCESS_TOKEN機密情報 なので、基本的にサーバーサイドに置く必要があります。Android アプリのコードや GitHub に直接埋め込むと、リバースエンジニアリングやリポジトリ漏えいで簡単に漏れます。本番環境では下図のように、自社の中間サーバーを通してアプリから Square API にアクセスする構成にしておくと安心です。

中間サーバーは Java / Spring Boot や Node.js、Cloudflare Worker など、HTTPS でアプリと Square API を中継できるものなら何でも構いません。Spring Boot で組む場合の取っ掛かりは はじめての Spring Boot 〜 JavaでWebアプリケーション に、サーバーレスで Webhook 受信や軽い API 中継を任せたい場合は Cloudflare Worker を使うと静的サイトにサーバーレスな受付口を作れて便利 にまとめています。

idempotency_key の役割

リクエスト本文に出てくる idempotency_key は、Square API に共通する重要なパラメータです。

べき等性(idempotency)」とは、「ある操作を 1 回行っても複数回行っても結果が同じである」ことを指す概念です。同じ idempotency_key でリクエストを再送しても、Square 側ではデバイスコードやチェックアウトを二重に作らず、同一の結果を返してくれます。通信タイムアウトや再送が発生したときに、二重決済や二重発行を防ぐためのキーです。

実装側では、たとえば次のように扱います。

  • リクエストを組み立てるタイミングで UUID v4 などで生成する
  • 成功するまでは同じキーを使ってリトライする
  • 成功したら新しい操作には新しいキーを発行する

デバイスコード発行後のレスポンス

{
  "device_code": {
    "id": "XXXXXXXXXXXXXXXX",
    "name": "Counter 1",
    "code": "XXXXXXXXXXXXXXXX",
    "product_type": "TERMINAL_API",
    "location_id": "XXXXXXXXXXXXXXXX",
    "pair_by": "2024-05-03T01:35:55.000Z",
    "created_at": "2024-05-03T01:30:56.121Z",
    "status": "UNPAIRED",
    "status_changed_at": "2024-05-03T01:30:55.000Z"
  }
}

Counter 1 は任意の文字列です。複数端末がある場合、例えば「レジカウンター 1 番のターミナル」のように名前を付けておくと管理しやすくなります。発行したデバイスコードは、すぐに使われないと期限切れとなって無効化されます。

デバイス コードが作成されたら、販売者はそのデバイス コードを使用して 5 分以内に Square ターミナルにサインインする必要があります。 デバイス コードが 5 分以内に使用されない場合、デバイス コードは期限切れになり、新しいデバイス コードをリクエストする必要があります。 最初のサインイン後は、デバイス コードを再利用して、その後のサインイン試行で同じ Square ターミナルにサインインできます。

販売者がSquareターミナルにサインインした後、デバイス コードを指定すると、ステータスが PAIRED に変わり、端末デバイスの device_id 値が利用可能になります。 デバイスIDはSquare Terminalの下面に記載されているシリアル番号です。

販売者がサインインすると、Webhook イベント通知を通じて POS アプリケーションに通知されます。

Square Terminal 端末を一度ログアウトしておき、発行されたデバイスコードでログインし直すと、ペアリング状態(status: PAIRED)になります。

運用上の注意(サンドボックス) サンドボックスアカウントでデバイスコードを発行しても、実機の Square Terminal にはログインできませんでした。本番アカウントで発行したデバイスコードを使う必要があります。 API のスキーマ確認だけならサンドボックスで足りますが、端末を絡めた疎通確認は本番アカウント前提で計画してください。

デバイスコードは、発行するたびに端末のログインをやり直す必要があるため、運用上は 一度発行したコードを使い回す のが基本です。

デバイス情報を取得(device_id の確認)

device_iddevice_code_id がわからなくなった時に使えるリクエストです。後述のチェックアウトでは device_id が必須なので、ペアリングが済んだらまずこのエンドポイントで控えておくと安心です。

curl https://connect.squareup.com/v2/devices \
  -H 'Square-Version: 2024-05-15' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json'

▼ レスポンス

{"devices": [{"id": "device:XXXXXXXXXXXXX","attributes": {"type": "TERMINAL","manufacturer": "Square","model": "Square Terminal (1st Gen, v2)","name": "Square ターミナル XXXX","manufacturers_id": "XXXXXXXXXXXXX","updated_at": "2024-05-18T02:35:32.503Z","version": "5.49.0024","merchant_token": "XXXXXXXXXXXXX"},"components": [{"type": "CARD_READER","card_reader_details": {"version": "3.61.116"}},{"type": "BATTERY","battery_details": {"visible_percent": 93,"external_power": "UNAVAILABLE"}},{"type": "WIFI","wifi_details": {"active": true,"ssid": "XXXXXXXXXXXXX","ip_address_v4": "192.168.175.238","secure_connection": "WPA_PSK","signal_strength": {"value": 3}}},{"type": "ETHERNET","ethernet_details": {"active": false}},{"type": "APPLICATION","application_details": {"application_type": "TERMINAL_API","version": "6.42","session_location": "XXXXXXXXXXXXX","device_code_id": "XXXXXXXXXXXXX"}},{"type": "APPLICATION","application_details": {"application_type": "SQUARE_POS","version": "6.40","session_location": "XXXXXXXXXXXXX"}}],"status": {"category": "AVAILABLE"}}]}

支払いフロー(公式仕様の整理)

公式の Square Terminal の支払いフロー を、自分が実装する立場で読み直すと次のような流れになります。

  1. POS クライアントで CreateDeviceCode を呼び出し、Square ターミナルとペアリングしてデバイス ID を取得する
  2. デバイスのペアリング成功の通知を待つ(Webhook または GET /v2/devices/codes/{id}
  3. POS クライアントが、購入者をチェックアウトするためのリクエストを Terminal API に送信する
  4. Square がチェックアウトの詳細を Square ターミナルに送信し、購入者に支払い画面が表示される
  5. 購入者が Square ターミナルに支払いカードを挿す・タップする・スワイプする
  6. Square ターミナルが Square 側に支払いを通知する
  7. Terminal API が、ターミナル チェックアウトの完了を POS バックエンドに通知する(Webhook またはポーリング)

device_id の入手元に注意 運用環境では、Devices API から返された device_id を使う必要があります。Square ターミナル背面に印字されたシリアル番号をリクエスト本文に入れることもできますが、販売者ダッシュボードから生成されたデバイスコードは device_id として使えません。Devices API でデバイス ID が生成された後は、販売者ダッシュボードからも参照できるようになります。

Get device code(デバイスコード一覧)

curl https://connect.squareup.com/v2/devices/codes/ \
  -H 'Square-Version: 2024-04-17' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json'

  {
  "device_codes": [
    {
      "id": "XXXXXXXXXXXXXXXX",
      "name": "Counter 1",
      "code": "XXXXXXXXXXXXXXXX",
      "device_id": "XXXXXXXXXXXXXXXX",
      "product_type": "TERMINAL_API",
      "location_id": "XXXXXXXXXXXXXXXX",
      "created_at": "2024-05-05T03:14:32.000Z",
      "status": "PAIRED",
      "status_changed_at": "2024-05-05T03:14:52.000Z",
      "paired_at": "2024-05-05T03:14:52.000Z"
    }
  ]
}

チェックアウト(金額表示)リクエスト

事前に device_id を取得しておきます。以下は日本での運用を想定した、ほぼ最小構成のリクエストです。

日本で運用するときのフィールド注意 tip_money などの米国以外でサポートされていないフィールドを入力するとエラーになります。日本向けでは currencyJPY にし、チップ関連のフィールドは送らない、もしくは tip_settings.allow_tipping = false を明示しておくのが無難です。

curl https://connect.squareup.com/v2/terminals/checkouts \
  -X POST \
  -H 'Square-Version: 2024-04-17' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "checkout": {
      "amount_money": {
        "amount": 1,
        "currency": "JPY"
      },
      "device_options": {
        "device_id": "XXXXXXXXXXXXXXXX"
      }
    },
    "idempotency_key": "XXXXXXXXXXXXXXXX"
  }'

idempotency_key の役割は 前述 の通りで、リクエストを再送しても二重決済にならないようにするためのキーです。

▼ レスポンス。会計をキャンセルするときには、ここで返ってくる checkout.id が必要になります。アプリ側でこの値を保持しておきます。

{
  "checkout": {
    "id": "XXXXXXXXXXXXXXXX",
    "amount_money": {
      "amount": 1,
      "currency": "JPY"
    },
    "device_options": {
      "device_id": "XXXXXXXXXXXXXXXX",
      "collect_signature": false,
      "tip_settings": {
        "allow_tipping": false
      },
      "skip_receipt_screen": false
    },
    "status": "PENDING",
    "created_at": "2024-05-07T00:05:46.371Z",
    "updated_at": "2024-05-07T00:05:46.371Z",
    "app_id": "XXXXXXXXXXXXXXXX",
    "location_id": "XXXXXXXXXXXXXXXX",
    "payment_type": "CARD_PRESENT",
    "payment_options": {
      "autocomplete": true
    }
  }
}

タイムアウトに注意 チェックアウトリクエスト後、数分間の間に決済が完了しないと自動でキャンセルされます。アプリ側でも、長時間ペンディングのまま残らないよう、適切なタイミングで状態確認・キャンセルを行う設計にしておきます。

チェックアウトのキャンセル処理

ユーザーが決済を中断した場合や、アプリ側で別の処理に切り替えたい場合は、チェックアウト ID を URL に渡して cancel エンドポイントを呼びます。

curl https://connect.squareup.com/v2/terminals/checkouts/jjJePcCcIXnqO/cancel \
  -X POST \
  -H 'Square-Version: 2024-04-17' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json'

URL の jjJePcCcIXnqO の部分が、先ほどのレスポンスで返ってきた checkout.id です。

{
  "checkout": {
    "id": "XXXXXXXXXXXXXXXX",
    "amount_money": {
      "amount": 1,
      "currency": "JPY"
    },
    "device_options": {
      "device_id": "XXXXXXXXXXXXXXXX",
      "collect_signature": false,
      "tip_settings": {
        "allow_tipping": false
      },
      "skip_receipt_screen": false
    },
    "status": "CANCEL_REQUESTED",
    "created_at": "2024-05-07T00:05:46.371Z",
    "updated_at": "2024-05-07T00:07:02.486Z",
    "app_id": "XXXXXXXXXXXXXXXX",
    "cancel_reason": "SELLER_CANCELED",
    "location_id": "XXXXXXXXXXXXXXXX",
    "payment_type": "CARD_PRESENT",
    "payment_options": {
      "autocomplete": true
    }
  }
}

レスポンスの statusCANCEL_REQUESTED に変わり、cancel_reasonSELLER_CANCELED が入ります。実際にターミナル側のキャンセルが反映されるまでには少し時間がかかるため、アプリ側ではポーリングか Webhook で最終ステータスを確認します。

そのほかにも様々な API が用意されています。決済結果の取得(GET /v2/terminals/checkouts/{checkout_id})や支払いの照会(Payments API)など、必要に応じて使い分けます。

Square の Webhook について

Webhook を使うと、API 実行時のログやイベントを自社サーバーで購読できます。

nameurlAPI version
terminal-webhookhttps://自社ドメイン/square/terminal-webhook.php2024-03-20

プロトタイプ開発では必須ではないので、当時は適当にエンドポイントを用意しただけで中身の実装は省略していました。本番運用では、ペアリング・決済完了・キャンセル通知などを Webhook で受け、アプリ側のポーリング負荷を下げるのが定石です。

Square の Webhook は、特定のイベントが Square システム内で発生した際に、登録された外部の URL に自動的に通知を送る仕組みです。例えば、新しい注文の作成や支払いの完了などのイベントがトリガーになります。

Webhook のメリット

  1. リアルタイム性: イベントが発生するとすぐに通知が送られるため、リアルタイムでのデータ同期や処理が可能です。
  2. 効率性: ポーリングなどで定期的に API を確認する必要がなく、必要な時にのみデータを受け取れるため、システムリソースの節約につながります。
  3. 自動化の強化: 特定のイベントに基づいて自動的にプロセスを実行できるため、作業の自動化が進みます。

Webhook の実装について

Webhook を利用するには、外部サーバーが必要です。このサーバーは Square からの通知を受信するエンドポイントとして機能し、受け取ったデータを元に必要な処理を行います。

サーバーは安全で信頼性の高い環境で運用する必要があり、HTTPS でセキュアに通信を行うことが求められます。また、受信したデータの検証も重要です。Square は送信された Webhook 通知が正当であることを確認するために署名キーを提供しているので、受信側で署名検証を実装しておきます。

Square の Webhook を利用するためには、Square のダッシュボードまたは API を通じて Webhook を設定し、受け取りたいイベントの種類を指定します。

実装に Spring Boot のような自前のサーバーをフルに持ち出さなくても、Cloudflare Worker などのサーバーレス基盤で軽く受けて、検証だけ通して内部処理に振り分ける構成もよく使います。具体的な作り方は Cloudflare Worker を使うと静的サイトにサーバーレスな受付口を作れて便利 にまとめているので、合わせてどうぞ。

まとめ

Square Terminal API を自社アプリのカードリーダー連携として使うときに、最低限押さえておきたいポイントを並べました。

  • ログインはアカウントではなく デバイスコード で行うことで、ターミナルをカードリーダー化できる
  • ACCESS_TOKEN は中間サーバーに隠す。アプリ本体には埋めない
  • device_idDevices API で取得し、運用ではダッシュボードのデバイスコードを device_id の代わりに使わない
  • リクエストには必ず idempotency_key を付け、再送・二重発行を防ぐ
  • 決済中断時は checkout.idcancel エンドポイントに渡してキャンセルする
  • 本番アカウントでないと端末ログインに使えないので、疎通確認の段取りに注意する
  • 本番運用ではポーリングではなく Webhook で結果通知を受け取る設計にしておく

Android アプリ側を含めた全体像を考えるときは、外部デバイス連携の作りを別記事の ESP32でBluetooth Classicを使ってAndroidと通信する も参考にしてください。Bluetooth と HTTP API という違いはありますが、「外部デバイスとアプリの状態を同期させる」という設計の組み立て方は共通する部分が多くあります。