--- title: "整合結帳" description: "整合我們的無縫結帳解決方案,為您的玩家提供日本本地支付方式。" source: "https://docs.tokenz.one/zh-TW/v2/checkout/get-started" api_version: "v2" locale: "zh-TW" version_status: "current" docs_stage: "prod" --- # 整合結帳 整合我們的無縫結帳解決方案,為您的玩家提供日本本地支付方式。 ## 自動整合(AI 輔助) 將下方提示詞貼到專案目錄中的 AI 程式編寫工具(Claude Code、Cursor、Codex 等)。代理會取得我們的機器可讀文件並完成完整的測試模式整合,包含結帳、Webhook,以及發生付款爭議或爭議款時作為交付證據的配送記錄;您只需檢視變更、填入 API 金鑰即可上線。 **更進一步 — 串接 Tokenz MCP。** 若你的 AI 工具支援 [MCP](https://modelcontextprotocol.io/),加入我們的 MCP 伺服器後,代理就能直接搜尋文件、建立測試結帳工作階段並驗證 Webhook 簽章 — 一套引導式、可自我驗證的串接流程。下方的提示詞在有無 MCP 的情況下都能運作。 #### Claude Code ```bash claude mcp add --transport http tokenz https://mcp.tokenz.one/mcp --header "Authorization: Bearer secret_test_YOUR_KEY_HERE" ``` #### Cursor 加入全域 `~/.cursor/mcp.json`(避免金鑰進入版本控制): ```json { "mcpServers": { "tokenz": { "url": "https://mcp.tokenz.one/mcp", "headers": { "Authorization": "Bearer secret_test_YOUR_KEY_HERE" } } } } ``` #### VS Code 加入 `.vscode/mcp.json` —— VS Code 會提示輸入金鑰,因此金鑰不會進入版本控制: ```json { "inputs": [ { "id": "tokenz-key", "type": "promptString", "description": "Tokenz secret_test_ key", "password": true } ], "servers": { "tokenz": { "type": "http", "url": "https://mcp.tokenz.one/mcp", "headers": { "Authorization": "Bearer ${input:tokenz-key}" } } } } ``` #### Codex 加入 `~/.codex/config.toml`。請注意,`bearer_token_env_var` 填寫的是環境變數的**名稱**,而非金鑰本身: ```toml [mcp_servers.tokenz] url = "https://mcp.tokenz.one/mcp" # 這是存放金鑰的環境變數「名稱」,而非金鑰本身。 bearer_token_env_var = "TOKENZ_SECRET_KEY" ``` 接著將該環境變數設定為你的測試金鑰: ```bash export TOKENZ_SECRET_KEY=secret_test_YOUR_KEY_HERE ``` ```text You are integrating Tokenz — a Merchant of Record (MoR) checkout — into this project, end to end, in TEST MODE. If a Tokenz MCP server is connected to you (you will see tools like `search_docs`, `get_openapi`, `create_test_checkout_session`, and `verify_webhook_signature`), use it throughout the steps below: prefer `search_docs`/`get_doc` over fetching raw doc URLs, take test cards from `list_test_cards`, and use `create_test_checkout_session` + `get_order` to prove the integration works against the real API. Everything below still applies either way. Before writing any code: 1. Detect my project's stack and find the code that grants purchased items to a customer (fulfillment). Then ask me the questions below immediately, and fetch https://docs.tokenz.one/llms-checkout.txt while you wait for my answers. It contains the v2 checkout guides and links to the raw OpenAPI contracts. Fetch the linked Checkout Session, Order, and Webhook JSON specs before implementing their requests and payloads; those raw specs are authoritative for fields, constraints, errors, and response formats. Use https://docs.tokenz.one/llms.txt to find any additional guides you need. Stay on v2 for this new integration. If you cannot fetch URLs, ask me to paste the relevant guides and raw contracts. 2. The questions: - What product(s) should the checkout sell (name, amount, currency)? - Which environment file should hold my Tokenz test secret key (secret_test_...) and webhook signing secret (e.g. .env.local)? I will fill in the values myself. Remind me that the key needs the `CreateCheckoutSession`, `GetOrder`, and `RegisterDeliveryRecord` permissions. - Confirm the framework/stack and the fulfillment code you found before patching anything. If the project has no fulfillment code yet, ask me how a paid order should be delivered and which ID identifies the customer. - If this project only runs locally: how should Tokenz reach the webhook endpoint? (Offer to set up a tunnel such as ngrok or cloudflared, or ask me for a deployed HTTPS URL.) Constraints (non-negotiable): - Never hardcode, log, or commit secrets — API keys and webhook signing secrets alike; read them from environment variables only. - Create Checkout Sessions server-side only; never expose the secret key to the browser. - Define products and prices server-side; never trust amounts sent by the browser. Amounts are in minor units, and every product item needs a taxCategory from the docs' enum. - Verify webhook signatures (Tokenz-Signature header) over the raw, unparsed request body; do not skip this. - Fulfill orders from verified webhook events (order.succeeded), never from the customer reaching the success page — redirects prove navigation, webhooks prove payment. Handle webhook retries idempotently, keyed on the event's top-level `id`; the same event can be delivered more than once. - Register a delivery record with Tokenz for every order you fulfill (POST /v2/order/{orderId}/delivery). This is a required part of the integration, not an extra: the delivery record is the proof of delivery sent to the bank or card issuer when a customer disputes a payment or files a chargeback, and an order without one has no evidence to defend it. Do not skip it, stub it, or offer it to me as optional. - Build redirect URLs from a configured base URL, not from request headers such as Origin. - Stay in Tokenz test mode until I explicitly ask to go live. - Keep changes minimal; do not refactor unrelated code. - Communicate with me in the language I use with you. Deliverables — verify each actually works before claiming it is done: 1. A server endpoint that creates a Tokenz Checkout Session and returns its URL. 2. A checkout button/flow that redirects the customer to the hosted checkout. 3. Success, processing, and canceled pages wired to the session's redirect URLs. (Some payment methods settle asynchronously — the processing page is not optional.) 4. A webhook endpoint that receives events, verifies the signature, and updates order state. As soon as the webhook URL is known, tell me so I can register it in the Tokenz Dashboard (remind me: register it in TEST mode; the Dashboard shows the signing secret once, and I will put it into the env file myself — never ask me to paste it in chat) — then keep building while I do that. 5. Delivery registration, for every order the webhook fulfills: - Order of operations: in the webhook handler, mark the event as processed, grant the items, and store the delivery record (or a job for it) as one transaction or one durable write, and only then acknowledge with 2xx. A retried webhook must find either nothing done or everything done, never granted items with no delivery record; if the project's storage cannot make that atomic, have the handler re-check fulfilled orders that lack a record and create it. Call the Delivery Registration API from outside the webhook response path (a queued job or background task), so a slow or unreachable API never delays the acknowledgement. - Payload: send the full payload the docs recommend — `status`, `progress`, `at`, `user`, `items` with `deliveredQuantity` (plus the in-app balance before and after, when the item is a currency), `cartContext`, `deliveryContext`, and `deliveryInfo` — built from the real fulfillment data, never from placeholders. `cartContext` and `deliveryContext` describe the customer's device: capture the IP address, user agent, and device ID from the customer's own requests (when the Checkout Session is created, and at delivery if the customer is present), and store them with the order. Never fill them from the webhook request, which comes from Tokenz's servers; leave out a field you do not have. - One record per delivery: register a failed delivery too (`status: failed`), send one record per delivery when an order is delivered in parts, and never let a retried webhook create a second record for the same delivery. - Response: treat every entry in `warnings` as a bug and fix it. A failed call must never repeat or undo the fulfillment. - Retries: each call creates a new record and there is no idempotency key, so keep every record Tokenz has not accepted in the project's own storage, in one of three states. Retryable (Tokenz answered 5xx): re-send automatically, including after a restart. Blocked (Tokenz answered 4xx): fix the cause before re-sending — 403 Forbidden means the key lacks the `RegisterDeliveryRecord` permission (on by default for secret keys), so tell me to use a secret key that has it; 401 Unauthorized means the key is missing, invalid, or revoked, so tell me to fix the key; re-send either once the key changes. Any other 4xx means the request is wrong, so fix it first. Needs review (no answer: timeout or dropped connection): never resend automatically; flag it for me to check. 6. A completed end-to-end test purchase using the test cards from the docs, with the webhook received and verified, the items granted once, and the delivery record accepted (201 Created, no warnings). If you have browser-automation tools, perform the test purchase yourself; otherwise give me exact steps. 7. A short go-live checklist for me: everything that must change to leave test mode (a live secret key with the permissions this integration uses, including `RegisterDeliveryRecord`; live webhook registration with its new signing secret; a stable public webhook URL; real tax/fulfillment handling), plus anything you stubbed. If any step is blocked (missing key, docs ambiguity, unsupported stack), stop and ask me instead of guessing. ``` ## 必要條件 請確保您已從 Tokenz 獲取 API 憑證(公鑰和金鑰)。您可以在 Tokenz Dashboard 的開發頁面找到您的憑證。 如果您想了解 Tokenz Checkout 的工作原理,請參閱 [Checkout](https://docs.tokenz.one/zh-TW/v2/) 了解更多詳細資訊。 整合期間請使用測試 API 金鑰(包含 `test_`,例如 `secret_test_...`):以測試金鑰建立的結帳工作階段會在測試模式下執行,不會產生實際扣款。上線時再切換為正式金鑰。 ## 設定伺服器 #### Python ```python import os from flask import Flask, request, redirect from flask_cors import CORS import requests # Replace the keys with yours SECRET_KEY = os.environ.get("SECRET_KEY", "secret_test_YOUR_KEY_HERE") app = Flask(__name__, static_url_path="") CORS(app) root = "../client/build" @app.route("/", methods=["GET"]) def index(): return redirect("http://localhost:3080") ``` #### PHP ```php run(); ``` #### Go ```go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.StaticFile("/demo.css", "../client/build/demo.css") router.StaticFile("/", "../client/build/index.html") router.StaticFile("/payment-success", "../client/build/payment-success.html") router.StaticFile("/payment-canceled", "../client/build/payment-canceled.html") ... router.Run(":5001") } ``` #### NodeJS ```js import express from "express"; const app = express(); const port = 3000; app.get("/", (req, res) => { res.send( "
", ); }); app.listen(port, () => { console.log(`Sandbox listening on port ${port}`); }); ``` ## 創建結帳工作階段 在您的伺服器上添加一個端點來創建結帳工作階段。結帳工作階段控制您的客戶在 Tokenz 託管的支付頁面上看到的內容,例如項目、訂單金額、貨幣和可用的支付方式。 ### 定義項目(line item) 總是將您的產品庫存的敏感資訊(如價格和可用性)保存在您的伺服器上,以防止客戶從客戶端進行操作。在創建結帳工作階段時定義產品資訊。 訂單總額一律依 `itemDetails` 中的價格計算。部分範例中出現的頂層 `amount` 欄位為選填,但若您提供該欄位,其值必須與 `itemDetails` 的總額相符,否則請求將被拒絕。 **稅金。** 本快速入門(及其範例載荷)基於 Merchant of Record(MoR)模式——即 Tokenz 帳戶的預設模式:Tokenz 會自動計算並代收稅金,您只需為每個商品設定 `taxCategory`。如果您的帳戶被特別設定為 Payment Gateway 模式,則每個 Checkout Session 都必須額外包含頂層 `tax` 物件(以最小貨幣單位表示,例如 `"tax": { "amount": 0, "currency": "USD" }`);缺少該欄位的請求會被拒絕並回傳 `order.tax-required`。如果不確定您的帳戶使用哪種模式,請與您的 Tokenz 聯絡人確認。 ### 提供成功和取消的 URL 指定成功和取消頁面的 URL,確保它們是可公開訪問的,以便 Tokenz 可以將客戶轉址過去。您也可以使用相同的 URL 來處理成功和取消狀態。 ### [非必要] 定義可用的支付方式 指定為此結帳向您的客戶提供的支付方式列表。如果未提供支付方式或提供了空列表,則所有可用的支付方式都將顯示給客戶。 ### 將客戶轉址到 Tokenz 結帳頁面 創建工作階段後,將您的客戶轉址到回應中返回的結帳頁面的 URL。 #### Python ```python @app.route("/create-tokenz-checkout", methods=['POST']) def create_tokenz_checkout(): CALLBACK_URL_PREFIX = request.headers['referer'] headers = {"Authorization": f'Bearer {SECRET_KEY}'} print(headers) payload = { "amount": { "currency": "JPY", "amount": 5200 }, "itemDetails": [ { "product": { "label": "ひとにぎりのエメラルド", "description": "80+8", "images": [ "https://images.ctfassets.net/z82qbo7cv7ia/1dWPbk5Qx2M1Qikj6Knyuc/e37b2c26829c0d30793a348ae3adb3b0/fake-pass.webp" ], "quantity": 3, "price": { "currency": "JPY", "amount": 1200 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } }, { "product": { "label": "エメラルドの荷車", "description": "100+10", "images": [], "quantity": 1, "price": { "currency": "JPY", "amount": 1600 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } } ], "customerInfo": {}, "successUrl": "http://localhost:9000/success", "pendingUrl": "http://localhost:9000/pending", "cancelUrl": "http://localhost:9000/cancel" } res = requests.post( "https://api.tokenz.one/v2/checkoutsession", json=payload, headers=headers ) session = res.json() return redirect(session["url"], 303) ``` #### PHP ```php $app->map(['get', 'post'], '/', function ($request, $response, $args) { $fileName = trim($request->getUri()->getBasePath(), '/'); if ($request->isPost()) { $client = new \GuzzleHttp\Client(); try { $checkoutSessionResponse = $client->request('POST', 'https://api.tokenz.one/v2/checkoutsession', [ 'body' => '{ "amount": { "currency": "JPY", "amount": 5200 }, "itemDetails": [ { "product": { "label": "ひとにぎりのエメラルド", "description": "80+8", "images": [ "https://images.ctfassets.net/z82qbo7cv7ia/1dWPbk5Qx2M1Qikj6Knyuc/e37b2c26829c0d30793a348ae3adb3b0/fake-pass.webp" ], "quantity": 3, "price": { "currency": "JPY", "amount": 1200 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } }, { "product": { "label": "エメラルドの荷車", "description": "100+10", "images": [], "quantity": 1, "price": { "currency": "JPY", "amount": 1600 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } } ], "customerInfo": {}, "successUrl": "http://localhost:9000/success", "pendingUrl": "http://localhost:9000/pending", "cancelUrl": "http://localhost:9000/cancel" }', 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer secret_test_YOUR_KEY_HERE', ], ]); $checkoutSession = json_decode($checkoutSessionResponse->getBody()); } catch (Exception $e) { echo $e; echo \GuzzleHttp\Psr7\Message::toString($e->getResponse()); } return $response->withRedirect($checkoutSession->url, 302); } else { $filePath = __DIR__ . '/../client/build/' . $fileName; switch (pathinfo($filePath, PATHINFO_EXTENSION)) { case 'css': $mimeType = 'text/css'; break; default: $mimeType = 'text/html'; } $body = $response->getBody(); $body->write(file_get_contents($filePath)); return $response ->withHeader('Content-Type', $mimeType . '; charset=UTF-8') ->withBody($body); } }); ``` #### Go ```go router.POST("/create-tokenz-checkout", func(c *gin.Context) { url := "https://api.tokenz.one/v2/checkoutsession" payload := strings.NewReader( `{ "amount": { "currency": "JPY", "amount": 5200 }, "itemDetails": [ { "product": { "label": "ひとにぎりのエメラルド", "description": "80+8", "images": [ "https://images.ctfassets.net/z82qbo7cv7ia/1dWPbk5Qx2M1Qikj6Knyuc/e37b2c26829c0d30793a348ae3adb3b0/fake-pass.webp" ], "quantity": 3, "price": { "currency": "JPY", "amount": 1200 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } }, { "product": { "label": "エメラルドの荷車", "description": "100+10", "images": [], "quantity": 1, "price": { "currency": "JPY", "amount": 1600 }, "taxCategory": "DIGITAL_GOODS_AND_SERVICES" } } ], "customerInfo": {}, "successUrl": "http://localhost:9000/success", "pendingUrl": "http://localhost:9000/pending", "cancelUrl": "http://localhost:9000/cancel" }`) req, _ := http.NewRequest("POST", url, payload) req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", os.Getenv("SECRET_KEY"))) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() bodyBytes, _ := io.ReadAll(resp.Body) var result Response if err := json.Unmarshal(bodyBytes, &result); err != nil { fmt.Println("Can not unmarshal JSON") } checkoutUrl := result.URL c.Redirect(http.StatusSeeOther, checkoutUrl) }) ``` #### NodeJS ```js app.post("/create-tokenz-checkout", (req, res) => { const API_ENDPOINT = "https://api.tokenz.one/v2/checkoutsession"; const payload = { amount: { currency: "JPY", amount: 5200, }, itemDetails: [ { product: { label: "ひとにぎりのエメラルド", description: "80+8", images: [ "https://images.ctfassets.net/z82qbo7cv7ia/1dWPbk5Qx2M1Qikj6Knyuc/e37b2c26829c0d30793a348ae3adb3b0/fake-pass.webp", ], quantity: 3, price: { currency: "JPY", amount: 1200, }, taxCategory: "DIGITAL_GOODS_AND_SERVICES", }, }, { product: { label: "エメラルドの荷車", description: "100+10", images: [], quantity: 1, price: { currency: "JPY", amount: 1600, }, taxCategory: "DIGITAL_GOODS_AND_SERVICES", }, }, ], customerInfo: {}, successUrl: "http://localhost:9000/success", pendingUrl: "http://localhost:9000/pending", cancelUrl: "http://localhost:9000/cancel", }; fetch(API_ENDPOINT, { method: "POST", body: JSON.stringify(payload), headers: { "content-type": "application/json", authorization: "Bearer " + process.env.SECRET_KEY, }, }) .then((response) => response.json()) .then((session) => { console.log(session); res.redirect(303, session.url); }); }); ``` ## 建立您的結帳頁面 ### 添加成功頁面 為您提供的結帳工作階段 `successUrl` 創建一個成功頁面,向您的客戶顯示訂單確認資訊或訂單詳細資訊。客戶成功完成結帳後,Tokenz 將轉址到此頁面。 ```html Thanks for your order!

感謝您的惠顧!如果您有任何問題,請發送電子郵件至 orders@example.com。

``` ### 添加待處理付款頁面 對於非同步支付方式(如便利店支付或銀行轉賬),支付可能會處於待處理狀態,時間從幾分鐘到幾天不等。為 `pendingUrl` 添加一個頁面。當客戶關閉異步支付的說明頁面後,Tokenz 會轉址到此頁面。 ```html 感謝您的訂購!

您的支付正在處理中!我們會在完成後第一時間通知您。 如果您有任何問題,請發送電子郵件至 orders@example.com.

``` ### 添加取消頁面 為 `cancelUrl` 添加另一個頁面。當客戶在結帳中點擊返回按鈕時,Tokenz 會轉址到此頁面。 ```html 結帳已取消

忘記添加商品到購物車了?繼續購物,然後回來支付!

``` ### 添加結帳按鈕 最後,添加一個頁面來顯示客戶訂單的預覽。允許他們審核或修改訂單—當他們被轉址到結帳頁面後,訂單內容即為確定狀態,除非創建新的結帳工作階段,否則無法修改訂單。 在您的訂單預覽頁面中添加一個按鈕。當您的客戶點擊此按鈕時,他們會被轉址到 Tokenz 託管的支付頁面。 ```html 購買新產品
Treasure chest

寶箱

$1,000
```