SOPSellers' Open Protocol

付款憑證

付款憑證是一筆交易用的付款授權:限定收款方、金額與期限,由客人的AI代理發給商家。

已上線預約時的付款憑證已上線;第一家商家(一位私人教練)已於2026-10-22啟用線上付款。

什麼是付款憑證

Shared Payment Token(SPT)是Stripe的產品:客人的AI代理用客人核准的付款方式,向某個收款方發出的付款授權。付款憑證只對這一筆、這個金額、這段時間、這個收款方有效。

收款方拿到憑證後,用它建立並確認一筆Stripe付款(PaymentIntent);之後的退款與報表,和直接收到客人的付款方式一樣處理。

三種角色

SPT有三種角色:收款的商家、發出憑證的代理,以及同時服務許多商家的平台。以下各用一張流程圖說明,並對照SOP的做法。

商家(Sellers)

商家收下代理送來的憑證並扣款。在SOP,這一步由SOP代你完成:收款方是Seller Tech U.S.A. LLC,經Stripe處理,款項依約定撥給你;手續費與撥款時程寫在報價單。

商家:SOP替你收下並扣款付款憑證
客人代理SOP商家核准付款方式取得給SOP的一次性憑證,金額與期限有上限連同預約送出憑證依核准金額扣款一次預約成立;依約定撥款給你提供服務
步驟
  1. 客人 → 代理:核准付款方式
  2. 代理 → 代理:取得給SOP的一次性憑證,金額與期限有上限
  3. 代理 → SOP:連同預約送出憑證
  4. SOP → SOP:依核准金額扣款一次
  5. SOP → 商家:預約成立;依約定撥款給你
  6. 商家 → 客人:提供服務

預約回應裡的payment_handlers,就是告訴代理「憑證要發給誰、付多少、何時到期」:

等待付款預約中的payment_handlers

JSON

[
  {
    "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"
    }
  }
]

已對本機服務實際執行:取自上方等待付款預約的booking.payment_handlers。本機的連接器副本,profile ID為佔位字串;正式環境的business_profile是賣方真實的Stripe profile ID。

代理(Agents)

代理向收款方取得Stripe profile,收集客人的付款方式,向Stripe發出SPT,再把憑證交給收款方。需要驗證時(例如3D Secure),代理要帶客人完成。逐步做法見替客人付款。

代理:用客人核准的憑證付款給SOP
客人代理SOPrequest_bookingpayment_handlers:com.stripe.payments與business profile;金額;期限請客人核准確切金額核准發出綁定該profile的共享付款憑證,限金額與期限帶憑證呼叫complete_booking已付款(已成立或待商家確認),或requires_action附3D Secure連結告知完成,或請客人完成3D Secure
步驟
  1. 代理 → SOP:request_booking
  2. SOP → 代理:payment_handlers:com.stripe.payments與business profile;金額;期限
  3. 代理 → 客人:請客人核准確切金額
  4. 客人 → 代理:核准
  5. 代理 → 代理:發出綁定該profile的共享付款憑證,限金額與期限
  6. 代理 → SOP:帶憑證呼叫complete_booking
  7. SOP → 代理:已付款(已成立或待商家確認),或requires_action附3D Secure連結
  8. 代理 → 客人:告知完成,或請客人完成3D Secure

用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)都是測試值。沒有任何實際扣款。

平台(Connect platforms)

同時服務許多商家的平台,要決定憑證發給誰:平台自己的profile,還是各商家的profile。SOP目前採用前者:所有商家共用Seller Tech U.S.A. LLC的付款profile,LLC以收款方身分扣款,再依約定撥款給商家。

平台:SOP以一個付款profile服務多家商家
客人代理SOP商家公開每家商家與要使用的付款profile核准付款方式發出給SOP profile的憑證連同某一家商家的預約送出以收款方身分扣款(Seller Tech U.S.A. LLC)確認預約通知預約;依約定撥款提供服務
步驟
  1. SOP → 代理:公開每家商家與要使用的付款profile
  2. 客人 → 代理:核准付款方式
  3. 代理 → 代理:發出給SOP profile的憑證
  4. 代理 → SOP:連同某一家商家的預約送出
  5. SOP → SOP:以收款方身分扣款(Seller Tech U.S.A. LLC)
  6. SOP → 代理:確認預約
  7. SOP → 商家:通知預約;依約定撥款
  8. 商家 → 客人:提供服務

讓每家商家擁有自己的付款profile、由商家本身當收款方、SOP只收平台費的做法,目前的狀態是規劃中,沒有實作。

適用地區與條款

Stripe目前列出的SPT適用地區是美國、加拿大與部分歐洲國家;各地區的客人、代理與商家能否使用,以Stripe公告為準。SPT仍是Stripe的preview功能,使用SPT即視為同意Stripe的相關服務條款。

卡片資料與PCI

SOP與商家都不會收到完整卡號、有效期限或安全碼,卡片資料由Stripe處理。預約紀錄只保留卡別與末四碼,商家在確認頁也看得到這兩項。