SOPSellers' Open Protocol

お客さまに代わって支払う

予約にオンライン決済が必要なとき、エージェントはお客さまが承認した支払い認証情報を取得し、complete_bookingを呼んで完了します。

公開中予約時のオンライン決済は提供中です。2026年10月22日、最初の事業者(パーソナルトレーナー)がオンライン決済を有効にしました。

このページは、エージェントを開発するエンジニア向けです。支払いが事業者にどう届くかを知りたい事業者の方はエージェント経由の予約を受けるをご覧ください。

支払いの流れ

事業者がオンライン決済に設定している場合、request_bookingは予約をすぐには確定せず、awaiting_paymentを返します。枠を確保し(既定は15分)、金額、通貨、期限、payment_handlersを添えます。エージェントはお客さまに正確な金額の承認を求め、支払い認証情報を取得してcomplete_bookingを呼びます。

エージェント:お客様が承認した認証情報でSOPに支払う
お客様エージェントSOPrequest_bookingpayment_handlers:com.stripe.paymentsとbusiness profile、金額、期限金額の承認を依頼承認そのプロファイル宛てにShared Payment Tokenを発行(金額・期限を限定)トークンでcomplete_booking支払い完了(確定または事業者の確認待ち)、またはrequires_actionと3Dセキュアのリンク完了を伝える、または3Dセキュアを案内
手順
  1. エージェント → SOP:request_booking
  2. SOP → エージェント:payment_handlers:com.stripe.paymentsとbusiness profile、金額、期限
  3. エージェント → お客様:金額の承認を依頼
  4. お客様 → エージェント:承認
  5. エージェント → エージェント:そのプロファイル宛てにShared Payment Tokenを発行(金額・期限を限定)
  6. エージェント → SOP:トークンでcomplete_booking
  7. SOP → エージェント:支払い完了(確定または事業者の確認待ち)、またはrequires_actionと3Dセキュアのリンク
  8. エージェント → お客様:完了を伝える、または3Dセキュアを案内

1. 支払いが必要な予約を作成する

予約の作成は、支払いが不要な場合と同じ呼び出しです。返されるstatusはawaiting_paymentで、amount(新台湾ドルの最小単位。NT$1,200は120000)、currency、expires_at、payment_handlersが付きます。価格が0のサービスには課金しません。

オンライン決済の予約を作成

curl

# <BUSINESS_ID> is a business whose payment mode is online (none is enabled yet). The booking is held until expires_at.
curl -sS -X POST "https://mcp.srct.tw/api/v1/businesses/<BUSINESS_ID>/bookings" \
  -H "User-Agent: my-agent/1.0" \
  -H "Content-Type: application/json" \
  -d '{"service_id":"small_group","slot":"<SLOT>","quantity":1,"customer_name":"Example Customer","phone":"0900000000","email":"customer@example.com"}'

Python

# <BUSINESS_ID> is a business whose payment mode is online (none is enabled yet). The booking is held until expires_at.
import json
import urllib.error
import urllib.request

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


BUSINESS_ID = "<BUSINESS_ID>"

status, body = call("POST", f"/api/v1/businesses/{BUSINESS_ID}/bookings", {
    "service_id": "small_group",
    "slot": "<SLOT>",  # the "slot" value from the availability response
    "quantity": 1,
    "customer_name": "Example Customer",
    "phone": "0900000000",
    "email": "customer@example.com",
})
show(status, body)
# status "awaiting_payment": body["booking"] has amount, currency, expires_at and payment_handlers.

Node.js

// <BUSINESS_ID> is a business whose payment mode is online (none is enabled yet). The booking is held until expires_at.
const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const BUSINESS_ID = "<BUSINESS_ID>";

(async () => {
  show(await call("POST", `/api/v1/businesses/${BUSINESS_ID}/bookings`, {
    service_id: "small_group",
    slot: "<SLOT>", // the "slot" value from the availability response
    quantity: 1,
    customer_name: "Example Customer",
    phone: "0900000000",
    email: "customer@example.com",
  }));
  // status "awaiting_payment": body.booking has amount, currency, expires_at and payment_handlers.
})();

レスポンス例

HTTP 201
{
  "ok": true,
  "booking": {
    "booking_id": "MCK3JUPZ",
    "status": "awaiting_payment",
    "status_text": "Waiting for online payment",
    "business_id": "example-studio",
    "business_name": "Example Studio (test data)",
    "service_id": "small_group",
    "service_name": "Small-group strength class",
    "slot": "2026-10-25T10:00",
    "start": "2026-10-25T10:00:00+08:00",
    "label": "10/25(日)10:00",
    "quantity": 1,
    "quantity_unit": "seat",
    "payment": "Online payment required: NT$600",
    "details": [],
    "payment_details": {
      "mode": "online",
      "status": "awaiting_payment",
      "amount": {
        "value": 60000,
        "currency": "twd",
        "text": "NT$600"
      },
      "collected_by": "Seller Tech U.S.A. LLC through Stripe",
      "expires_at": "2026-10-22T01:02:00+08:00"
    },
    "amount": 60000,
    "currency": "twd",
    "expires_at": "2026-10-22T01:02:00+08:00",
    "payment_handlers": [
      {
        "id": "stripe_payments",
        "type": "com.stripe.payments",
        "version": "2026-06-25",
        "spec": "https://docs.stripe.com/agentic-commerce/ucp/stripe-payments-handler",
        "schema": "https://ucp.stripe.com/payments/2026-06-25/schema.json",
        "available_instruments": [
          {
            "type": "card"
          },
          {
            "type": "link",
            "config": {
              "network_id": "profile_xxxx"
            }
          }
        ],
        "config": {
          "environment": "testmode",
          "business_profile": "profile_xxxx"
        }
      }
    ],
    "payment_link": {
      "url": "https://mcp.srct.tw/pay/example-studio/MCK3JUPZ?t=<PAY_TOKEN>",
      "protocols": [
        "x402",
        "mpp"
      ],
      "amount_usd": "18.53",
      "fx": {
        "twd_per_usd": 32.385,
        "source": "Bank of Taiwan board rate, USD spot buying",
        "as_of": "2026-10-21T23:42:00+08:00"
      },
      "networks": [
        "base-sepolia (USDC, x402 or MPP evm)",
        "tempo testnet (MPP tempo)"
      ],
      "testnet": true
    },
    "online_cancel_until": "2026-10-25T08:00:00+08:00"
  },
  "status_token": "<STATUS_TOKEN>",
  "status_token_note": "Needed with booking_id to check, pay, change or cancel this booking later. It gives access to this booking only, and … does not show it again.",
  "next_step": "Get the customer's approval for NT$600 and a payment credential from one of payment_handlers (for com.stripe.payments: a Stripe shared payment token issued to business_profile, limited to this amount and currency), then call complete_booking before expires_at. Unpaid bookings are released at expires_at.",
  "contact_note": "If this booking cannot be cancelled with cancel_booking, the customer can contact the business directly: Test data, no real contact"
}

ローカル環境で実行済み:合成のテスト事業者とプレースホルダーのStripe profile(profile_xxxx)を使ったコネクターのローカルコピーで実行したため、environmentはtestmodeです。予約作成の段階ではStripeを呼びません。status_tokenと?t=の支払いトークンはプレースホルダーに置き換えています。

payment_handlersは、UCP形式のcom.stripe.paymentsです。idはstripe_payments、使える支払い手段はcardとlinkで、config.business_profileは、エージェントが認証情報を発行するときに指定するStripeのプロファイルです。

2. 支払い認証情報を取得する

エージェントは、お客さまが承認した支払い方法を使って、StripeでShared Payment Token(SPT)を発行します。受け取り先は前の手順のbusiness_profileで、通貨、上限金額(予約の合計額と同額)、有効期限を限定します。この手順はエージェント自身のStripeアカウントで実行し、SOPは経由しません。

エージェント側:事業者向けにShared Payment Tokenを発行

curl

# Run by the agent with its own Stripe key. profile_..., amount and the other values come from the awaiting_payment booking.
curl https://api.stripe.com/v1/shared_payment/issued_tokens \
  -u "sk_test_...:" \
  -H "User-Agent: my-agent/1.0" \
  -H "Stripe-Version: 2026-09-30.preview" \
  -d "payment_method=pm_..." \
  -d "seller_details[network_business_profile]=profile_..." \
  -d "usage_limits[currency]=twd" \
  -d "usage_limits[expires_at]=1798761600" \
  -d "usage_limits[max_amount]=60000" \
  --data-urlencode "return_url=https://example.com/agent-checkout/return"

Python

import base64
import json
import urllib.parse
import urllib.request

SECRET_KEY = "sk_test_..."  # the agent's own Stripe key

form = urllib.parse.urlencode({
    "payment_method": "pm_...",
    "seller_details[network_business_profile]": "profile_...",  # payment_handlers[0].config.business_profile
    "usage_limits[currency]": "twd",                            # booking.currency
    "usage_limits[expires_at]": "1798761600",                   # Unix time, later than your complete_booking call
    "usage_limits[max_amount]": "60000",                        # booking.amount
    "return_url": "https://example.com/agent-checkout/return",
}).encode()

req = urllib.request.Request(
    "https://api.stripe.com/v1/shared_payment/issued_tokens",
    data=form,
    method="POST",
    headers={
        "Authorization": "Basic " + base64.b64encode(f"{SECRET_KEY}:".encode()).decode(),
        "Stripe-Version": "2026-09-30.preview",
        "User-Agent": "my-agent/1.0",
    },
)
with urllib.request.urlopen(req) as res:
    print(json.dumps(json.loads(res.read()), indent=2))  # the "id" (spt_...) is what you send to complete_booking

Node.js

const SECRET_KEY = "sk_test_..."; // the agent's own Stripe key

(async () => {
  const res = await fetch("https://api.stripe.com/v1/shared_payment/issued_tokens", {
    method: "POST",
    headers: {
      Authorization: "Basic " + Buffer.from(`${SECRET_KEY}:`).toString("base64"),
      "Stripe-Version": "2026-09-30.preview",
      "User-Agent": "my-agent/1.0",
    },
    body: new URLSearchParams({
      payment_method: "pm_...",
      "seller_details[network_business_profile]": "profile_...", // payment_handlers[0].config.business_profile
      "usage_limits[currency]": "twd", // booking.currency
      "usage_limits[expires_at]": "1798761600", // Unix time, later than your complete_booking call
      "usage_limits[max_amount]": "60000", // booking.amount
      return_url: "https://example.com/agent-checkout/return",
    }),
  });
  console.log(JSON.stringify(await res.json(), null, 2)); // the "id" (spt_...) is what you send to complete_booking
})();

当社では未実行。提供元のドキュメントに基づく:未実行:エージェント自身のStripeシークレットキーが必要ですが、当社にはありません。パラメーター名とStripe-Versionヘッダーは、Stripeのエージェント向けShared Payment Tokenドキュメントに従っています。

SPTはStripeのプレビュー機能のため、APIバージョンとパラメーターはStripeのドキュメントに従ってください。対象地域と規約は支払い認証情報をご覧ください。

3. 予約を完了する

complete_bookingを呼び、business_id、booking_id、status_token、paymentを渡します。handler_idはstripe_payments、instrument_typeはcardまたはlink、credentialは{"type": "stripe_payment_token", "token": "spt_…"}です。

Shared Payment Tokenで支払いを完了する

curl

# Reuse the same Idempotency-Key when you retry this request after a network error.
curl -sS -X POST "https://mcp.srct.tw/api/v1/businesses/<BUSINESS_ID>/bookings/<BOOKING_ID>/complete" \
  -H "User-Agent: my-agent/1.0" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <STATUS_TOKEN>" \
  -H "Idempotency-Key: pay-$(uuidgen)" \
  -d '{"payment":{"instruments":[{"id":"instr_1","handler_id":"stripe_payments","type":"card","selected":true,"credential":{"type":"stripe_payment_token","token":"spt_..."}}]}}'

Python

import json
import urllib.error
import urllib.request
import uuid

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


BUSINESS_ID = "<BUSINESS_ID>"
BOOKING_ID = "<BOOKING_ID>"      # from the create response
STATUS_TOKEN = "<STATUS_TOKEN>"  # from the create response (shown once)

payment = {
    "instruments": [{
        "id": "instr_1",
        "handler_id": "stripe_payments",  # an id from payment_handlers
        "type": "card",
        "selected": True,
        "credential": {"type": "stripe_payment_token", "token": "spt_..."},  # the SPT the agent issued
    }]
}
headers = {
    "Authorization": f"Bearer {STATUS_TOKEN}",
    "Idempotency-Key": str(uuid.uuid4()),  # reuse the same key when you retry after a network error
}

show(*call("POST", f"/api/v1/businesses/{BUSINESS_ID}/bookings/{BOOKING_ID}/complete", {"payment": payment}, headers))

Node.js

const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const BUSINESS_ID = "<BUSINESS_ID>";
const BOOKING_ID = "<BOOKING_ID>"; // from the create response
const STATUS_TOKEN = "<STATUS_TOKEN>"; // from the create response (shown once)

const payment = {
  instruments: [{
    id: "instr_1",
    handler_id: "stripe_payments", // an id from payment_handlers
    type: "card",
    selected: true,
    credential: { type: "stripe_payment_token", token: "spt_..." }, // the SPT the agent issued
  }],
};
const headers = {
  Authorization: `Bearer ${STATUS_TOKEN}`,
  "Idempotency-Key": `pay-${Date.now()}`, // reuse the same key when you retry after a network error
};

(async () => {
  show(await call("POST", `/api/v1/businesses/${BUSINESS_ID}/bookings/${BOOKING_ID}/complete`, { payment }, headers));
})();

レスポンス例

HTTP 200
{
  "ok": true,
  "booking": {
    "booking_id": "WQEMHR2E",
    "status": "pending",
    "status_text": "Waiting for the business to confirm",
    "business_id": "example-studio",
    "business_name": "Example Studio (test data)",
    "service_id": "small_group",
    "service_name": "Small-group strength class",
    "slot": "2026-10-25T10:00",
    "start": "2026-10-25T10:00:00+08:00",
    "label": "10/25(日)10:00",
    "quantity": 1,
    "quantity_unit": "seat",
    "payment": "Paid online: NT$600",
    "details": [],
    "payment_details": {
      "mode": "online",
      "status": "paid",
      "amount": {
        "value": 60000,
        "currency": "twd",
        "text": "NT$600"
      },
      "collected_by": "Seller Tech U.S.A. LLC through Stripe",
      "paid_at": "2026-10-22T11:51:00+08:00",
      "card": {
        "brand": "visa",
        "last4": "4242"
      },
      "method": "stripe_spt",
      "transaction_id": "pi_1"
    },
    "why_pending": "First booking with this business from these contact details, so the business confirms it.",
    "confirm_by": "2026-10-22T12:01:00+08:00",
    "hold": "Bookings that need the business's confirmation are held for 10 minutes. If the business does not confirm by then, the hold is released automatically and the customer can choose another slot.",
    "online_cancel_until": "2026-10-25T08:00:00+08:00"
  },
  "payment": {
    "status": "succeeded",
    "transaction_id": "pi_1",
    "instrument_type": "card"
  }
}

レスポンスはサービスの自動テストから取得:コネクターのテスト環境から取得しました。コネクターのコードはそのまま動かし、Stripeはテストスイートの偽Stripeサーバーに置き換えています。transaction_id(pi_1)と決済手数料(USD1.20、NT$39換算)はテスト値です。実際の課金は一切ありません。

レスポンスのpayment.statusは次のいずれかです。支払いが拒否された場合はエラーpayment_declined(RESTでは402)が返り、再試行可能と示されます。

状態意味エージェントの対応
succeeded支払い済み。予約は確定(confirmed)するか、事業者の確認ルールに従って確認待ち(pending)になります。お客さまに結果を伝える。
requires_actionお客さまによる認証(3Dセキュアなど)が必要です。次のセクションを参照。
processingStripeで処理中です。少し待ってからget_booking_statusで確認する。
payment_declined(エラー)支払いが拒否され、予約はawaiting_paymentに戻ります。お客さまに別の支払い方法を依頼し、期限内に新しい認証情報で呼び直す。

認証が必要なとき(3Dセキュア)

銀行が3Dセキュアを求めると、payment.statusはrequires_actionになり、redirect_urlが付きます。お客さまにそのURLで認証を完了してもらい、エージェントは同じ認証情報でもう一度complete_bookingを呼びます。サーバーは元の支払いを照会するので、二重に課金されることはありません。

3D Secure:支払いがrequires_actionを返す場合

curl

# Reuse the same Idempotency-Key when you retry this request after a network error.
# payment.status "requires_action": show payment.redirect_url to the customer. When they have finished, send this
# request again with the same token (the same Idempotency-Key works: a requires_action result is not stored).
curl -sS -X POST "https://mcp.srct.tw/api/v1/businesses/<BUSINESS_ID>/bookings/<BOOKING_ID>/complete" \
  -H "User-Agent: my-agent/1.0" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <STATUS_TOKEN>" \
  -H "Idempotency-Key: pay-$(uuidgen)" \
  -d '{"payment":{"instruments":[{"id":"instr_1","handler_id":"stripe_payments","type":"card","selected":true,"credential":{"type":"stripe_payment_token","token":"spt_..."}}]}}'

Python

import json
import urllib.error
import urllib.request
import uuid

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


BUSINESS_ID = "<BUSINESS_ID>"
BOOKING_ID = "<BOOKING_ID>"      # from the create response
STATUS_TOKEN = "<STATUS_TOKEN>"  # from the create response (shown once)

payment = {
    "instruments": [{
        "id": "instr_1",
        "handler_id": "stripe_payments",  # an id from payment_handlers
        "type": "card",
        "selected": True,
        "credential": {"type": "stripe_payment_token", "token": "spt_..."},  # the SPT the agent issued
    }]
}
headers = {
    "Authorization": f"Bearer {STATUS_TOKEN}",
    "Idempotency-Key": str(uuid.uuid4()),  # reuse the same key when you retry after a network error
}

show(*call("POST", f"/api/v1/businesses/{BUSINESS_ID}/bookings/{BOOKING_ID}/complete", {"payment": payment}, headers))
# payment.status "requires_action": show payment.redirect_url to the customer. When they have finished,
# send this request again with the same token (the same Idempotency-Key works: a requires_action result is not stored)
# response). The booking is not charged twice.

Node.js

const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const BUSINESS_ID = "<BUSINESS_ID>";
const BOOKING_ID = "<BOOKING_ID>"; // from the create response
const STATUS_TOKEN = "<STATUS_TOKEN>"; // from the create response (shown once)

const payment = {
  instruments: [{
    id: "instr_1",
    handler_id: "stripe_payments", // an id from payment_handlers
    type: "card",
    selected: true,
    credential: { type: "stripe_payment_token", token: "spt_..." }, // the SPT the agent issued
  }],
};
const headers = {
  Authorization: `Bearer ${STATUS_TOKEN}`,
  "Idempotency-Key": `pay-${Date.now()}`, // reuse the same key when you retry after a network error
};

(async () => {
  show(await call("POST", `/api/v1/businesses/${BUSINESS_ID}/bookings/${BOOKING_ID}/complete`, { payment }, headers));
})();
// payment.status "requires_action": show payment.redirect_url to the customer. When they have finished,
// send this request again with the same token (the same Idempotency-Key works: a requires_action result is not stored)
// response). The booking is not charged twice.

レスポンス例

HTTP 200
{
  "ok": true,
  "booking": {
    "booking_id": "TJXN8ZHY",
    "status": "awaiting_payment",
    "status_text": "Waiting for the customer to finish payment verification",
    "business_id": "example-studio",
    "business_name": "Example Studio (test data)",
    "service_id": "small_group",
    "service_name": "Small-group strength class",
    "slot": "2026-10-25T10:00",
    "start": "2026-10-25T10:00:00+08:00",
    "label": "10/25(日)10:00",
    "quantity": 1,
    "quantity_unit": "seat",
    "payment": "Online payment required: NT$600",
    "details": [],
    "payment_details": {
      "mode": "online",
      "status": "requires_action",
      "amount": {
        "value": 60000,
        "currency": "twd",
        "text": "NT$600"
      },
      "collected_by": "Seller Tech U.S.A. LLC through Stripe",
      "expires_at": "2026-10-22T01:07:00+08:00",
      "transaction_id": "pi_7"
    },
    "amount": 60000,
    "currency": "twd",
    "expires_at": "2026-10-22T01:07:00+08:00",
    "payment_handlers": [
      {
        "id": "stripe_payments",
        "type": "com.stripe.payments",
        "version": "2026-06-25",
        "spec": "https://docs.stripe.com/agentic-commerce/ucp/stripe-payments-handler",
        "schema": "https://ucp.stripe.com/payments/2026-06-25/schema.json",
        "available_instruments": [
          {
            "type": "card"
          },
          {
            "type": "link",
            "config": {
              "network_id": "profile_xxxx"
            }
          }
        ],
        "config": {
          "environment": "testmode",
          "business_profile": "profile_xxxx"
        }
      }
    ],
    "online_cancel_until": "2026-10-25T08:00:00+08:00"
  },
  "payment": {
    "status": "requires_action",
    "transaction_id": "pi_7",
    "instrument_type": "card",
    "redirect_url": "https://hooks.stripe.test/3ds/pi_7"
  },
  "next_step": "Open redirect_url for the customer to finish payment verification, then call complete_booking again with the same credential."
}

レスポンスはサービスの自動テストから取得:コネクターのテスト環境から取得し、Stripeはテストスイートの偽サーバーに置き換えています。redirect_urlは偽サーバーのプレースホルダーで、実際にはStripeのURLになります。顧客が認証を終えてリクエストを再送するまで、予約はawaiting_paymentのままです。

予約と支払いの状態を確認する

get_booking_status(RESTではGET bookings/{booking_id})は、予約の状態と、支払い・返金の情報を返します。支払いが処理中、または認証待ちのときは、先にStripeと同期します。

予約状況の確認

curl

curl -sS "https://mcp.srct.tw/api/v1/businesses/<BUSINESS_ID>/bookings/<BOOKING_ID>" \
  -H "User-Agent: my-agent/1.0" \
  -H "Authorization: Bearer <STATUS_TOKEN>"

Python

import json
import urllib.error
import urllib.request

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


BUSINESS_ID = "<BUSINESS_ID>"
BOOKING_ID = "<BOOKING_ID>"      # from the create response
STATUS_TOKEN = "<STATUS_TOKEN>"  # from the create response (shown once)
AUTH = {"Authorization": f"Bearer {STATUS_TOKEN}"}

show(*call("GET", f"/api/v1/businesses/{BUSINESS_ID}/bookings/{BOOKING_ID}", headers=AUTH))

Node.js

const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const BUSINESS_ID = "<BUSINESS_ID>";
const BOOKING_ID = "<BOOKING_ID>"; // from the create response
const STATUS_TOKEN = "<STATUS_TOKEN>"; // from the create response (shown once)
const AUTH = { Authorization: `Bearer ${STATUS_TOKEN}` };

(async () => {
  show(await call("GET", `/api/v1/businesses/${BUSINESS_ID}/bookings/${BOOKING_ID}`, undefined, AUTH));
})();

レスポンス例

HTTP 200
{
  "ok": true,
  "booking": {
    "booking_id": "2QAC3NW5",
    "status": "pending",
    "status_text": "Waiting for the business to confirm",
    "business_id": "coach-dunken",
    "business_name": "Dunken教練",
    "service_id": "pt",
    "service_name": "PT一對一教練課",
    "slot": "2026-10-25T08:00",
    "start": "2026-10-25T08:00:00+08:00",
    "label": "10/25(日)08:00",
    "quantity": 1,
    "quantity_unit": "位",
    "payment": "Pay at the venue",
    "details": [
      "老師:Dunken"
    ],
    "why_pending": "First booking with this business from these contact details, so the business confirms it.",
    "confirm_by": "2026-10-22T01:00:00+08:00",
    "hold": "Bookings that need the business's confirmation are held for 10 minutes. If the business does not confirm by then, the hold is released automatically and the customer can choose another slot.",
    "online_cancel_until": "2026-10-24T08:00:00+08:00"
  }
}

ローカル環境で実行済み:コネクターのローカルコピーで実行。リクエストのstatus_tokenは、予約作成時に受け取った値に置き換えてください。

安全に再試行する

書き込み系のツールはidempotency_key(8〜64文字。英数字と_ . : -が使えます)を受け取ります。RESTではIdempotency-Keyヘッダーを使います。同じキーで同じ内容なら最初の結果を返し、同じキーで内容が異なる場合はidempotency_conflict(409)を返します。記録は48時間保持します。また、同じ予約に同じ認証情報を使っても、課金は1回だけです。

冪等キーによる安全な再試行

curl

# This creates a real booking at a real business. Use a slot value from the availability response.
KEY="retry-$(uuidgen)"  # any unique string of 8-64 characters: letters, digits, _ . : -
URL=https://mcp.srct.tw/api/v1/businesses/<BUSINESS_ID>/bookings
BODY='{"service_id":"pt","slot":"<SLOT>","quantity":1,"customer_name":"Example Customer","phone":"0900000000","email":"customer@example.com"}'

# 1st call: creates the booking (201)
curl -sS -X POST "$URL" -H "User-Agent: my-agent/1.0" -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" -d "$BODY"
echo
# 2nd call: same key, same body: the original result again, with "replayed": true (200)
curl -sS -X POST "$URL" -H "User-Agent: my-agent/1.0" -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" -d "$BODY"
echo
# 3rd call: same key, different body: idempotency_conflict (409)
curl -sS -X POST "$URL" -H "User-Agent: my-agent/1.0" -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" -d '{"service_id":"pt","slot":"<SLOT>","quantity":1,"customer_name":"Example Customer","phone":"0900000000","email":"customer@example.com","note":"different"}'
echo

Python

# This creates a real booking at a real business. Use a slot value from the availability response.
import json
import urllib.error
import urllib.request
import uuid

BASE = "https://mcp.srct.tw"


def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        BASE + path,
        data=None if body is None else json.dumps(body).encode(),
        method=method,
        headers={"User-Agent": "my-agent/1.0", "Content-Type": "application/json", **(headers or {})},
    )
    try:
        with urllib.request.urlopen(req) as res:
            return res.status, json.loads(res.read())
    except urllib.error.HTTPError as err:  # 4xx and 5xx responses still carry a JSON body
        return err.code, json.loads(err.read())


def show(status, body):
    print("HTTP", status)
    print(json.dumps(body, ensure_ascii=False, indent=2))


BUSINESS_ID = "<BUSINESS_ID>"
path = f"/api/v1/businesses/{BUSINESS_ID}/bookings"
booking = {
    "service_id": "pt",
    "slot": "<SLOT>",
    "quantity": 1,
    "customer_name": "Example Customer",
    "phone": "0900000000",
    "email": "customer@example.com",
}
headers = {"Idempotency-Key": str(uuid.uuid4())}  # any unique string of 8-64 characters: letters, digits, _ . : -

show(*call("POST", path, booking, headers))  # 1st call: creates the booking (201)
show(*call("POST", path, booking, headers))  # same key, same body: the original result again, "replayed": true (200)
show(*call("POST", path, {**booking, "note": "different"}, headers))  # same key, different body: idempotency_conflict (409)

Node.js

// This creates a real booking at a real business. Use a slot value from the availability response.
const BASE = "https://mcp.srct.tw";

async function call(method, path, body, headers = {}) {
  const res = await fetch(BASE + path, {
    method,
    headers: { "User-Agent": "my-agent/1.0", "Content-Type": "application/json", ...headers },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  return { status: res.status, body: await res.json() };
}

function show({ status, body }) {
  console.log("HTTP", status);
  console.log(JSON.stringify(body, null, 2));
}

const BUSINESS_ID = "<BUSINESS_ID>";
const path = `/api/v1/businesses/${BUSINESS_ID}/bookings`;
const booking = {
  service_id: "pt",
  slot: "<SLOT>",
  quantity: 1,
  customer_name: "Example Customer",
  phone: "0900000000",
  email: "customer@example.com",
};
const headers = { "Idempotency-Key": `retry-${Date.now()}` }; // any unique string of 8-64 characters: letters, digits, _ . : -

(async () => {
  show(await call("POST", path, booking, headers)); // 1st call: creates the booking (201)
  show(await call("POST", path, booking, headers)); // same key, same body: the original result again, "replayed": true (200)
  show(await call("POST", path, { ...booking, note: "different" }, headers)); // same key, different body: idempotency_conflict (409)
})();

レスポンス例

HTTP 201
{
  "ok": true,
  "booking": {
    "booking_id": "KH6RZNWP",
    "status": "pending",
    "status_text": "Waiting for the business to confirm",
    "business_id": "coach-dunken",
    "business_name": "Dunken教練",
    "service_id": "pt",
    "service_name": "PT一對一教練課",
    "slot": "2026-10-25T08:00",
    "start": "2026-10-25T08:00:00+08:00",
    "label": "10/25(日)08:00",
    "quantity": 1,
    "quantity_unit": "位",
    "payment": "Pay at the venue",
    "details": [
      "老師:Dunken"
    ],
    "why_pending": "First booking with this business from these contact details, so the business confirms it.",
    "confirm_by": "2026-10-22T01:00:00+08:00",
    "hold": "Bookings that need the business's confirmation are held for 10 minutes. If the business does not confirm by then, the hold is released automatically and the customer can choose another slot.",
    "online_cancel_until": "2026-10-24T08:00:00+08:00"
  },
  "status_token": "<STATUS_TOKEN>",
  "status_token_note": "Needed with booking_id to check, pay, change or cancel this booking later. It gives access to this booking only, and … does not show it again.",
  "contact_note": "If this booking cannot be cancelled with cancel_booking, the customer can contact the business directly: …"
}

HTTP 200
{
  "ok": true,
  "booking": …,
  "status_token": "<STATUS_TOKEN>",
  "status_token_note": …,
  "contact_note": …,
  "replayed": true
}

HTTP 409
{
  "error": {
    "code": "idempotency_conflict",
    "message": "This idempotency_key was already used with different parameters. Use a new key for a different request."
  }
}

ローカル環境で実行済み:表示用に省略しています。…は省略部分を示します。コネクターのローカルコピーで実行。2つ目のレスポンスは保存済みの元のレスポンス(同じ予約・同じstatus_token)に"replayed": trueが付いたもので、重複部分は…で示しています。

期限切れと返金

フロー図と返金のレスポンスはエージェント経由の予約を受けるをご覧ください。

認証情報が取り消された・期限切れの場合

エージェントは、Shared Payment Tokenを使用前ならいつでも取り消せます。complete_bookingが取り消し済み・期限切れ・使用済みのトークンを受け取ると、token_invalidを返し、課金はしません。お客さまに再度承認してもらい、新しいトークンを発行してください。予約自体のexpires_atを過ぎている場合、枠はすでに解放されています。

状況の更新

SOPはエージェントにWebhookを送りません。予約の状況は、status_tokenを付けてget_booking_statusを呼ぶ(またはRESTでその予約をGETする)ことで確認します。カード決済の結果は、complete_bookingのレスポンスに含まれます。

連携をテストする

本番サービスでのカード決済は実際の事業者への支払いになるため、テストには使わないでください。テストが必要な場合は、テスト用に設定した事業者を用意しますので、お問い合わせください。