--- title: "Checkout 연동하기" description: "원활한 Checkout 연동으로 플레이어에게 세계 각국의 다양한 결제 수단을 제공하세요." source: "https://docs.tokenz.one/ko-KR/v2/checkout/get-started" api_version: "v2" locale: "ko-KR" version_status: "current" docs_stage: "prod" --- # Checkout 연동하기 원활한 Checkout 연동으로 플레이어에게 세계 각국의 다양한 결제 수단을 제공하세요. ## AI를 이용한 자동 연동 프로젝트 디렉터리에서 AI 코딩 도구(Claude Code, Cursor, Codex 등)에 아래 프롬프트를 복사하세요. 에이전트가 기계 판독용 문서를 읽고 Checkout, 웹훅, 분쟁·차지백 시 지급 증빙이 되는 아이템 지급 기록을 포함한 테스트 모드 연동을 구현합니다. 변경 사항을 검토하고 API 키를 추가한 후 운영에 적용하세요. **Tokenz MCP를 연결하면 더 편리합니다.** AI 도구가 [MCP](https://modelcontextprotocol.io/)를 지원하면 서버를 추가하세요. 에이전트가 문서 검색, 테스트 Checkout Session 생성, 웹훅 서명 검증을 직접 수행할 수 있습니다. 아래 프롬프트는 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" # The NAME of the env var that holds your key — not the key itself. 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 비밀 키를 준비하세요. 키는 대시보드의 Developers → API keys에서 관리할 수 있습니다. Tokenz Checkout의 동작 방식은 [Checkout](https://docs.tokenz.one/ko-KR/v2/checkout)을 참고하세요. 개발 및 테스트에는 테스트 비밀 키(`test_` 포함, 예: `secret_test_...`)를 사용하세요. 이 키로 생성한 Checkout Session에서는 실제 결제가 발생하지 않습니다. 실제 결제를 받으려면 라이브 비밀 키로 전환하세요. ## 서버 설정 #### 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}`); }); ``` ## Checkout Session 생성 서버에 Checkout Session을 생성하는 엔드포인트를 추가하세요. Checkout Session은 상품 항목, 주문 금액, 통화, 사용 가능한 결제 수단 등 Tokenz 호스팅 결제 페이지에 표시할 내용을 제어합니다. ### 상품 항목 정의 가격과 재고 등 민감한 상품 정보는 고객이 클라이언트에서 조작할 수 없도록 항상 서버에 보관하세요. Checkout Session을 생성할 때 상품 정보를 지정합니다. 주문 합계는 항상 `itemDetails`의 가격으로 계산됩니다. 일부 예제의 최상위 `amount`는 선택 사항이지만 지정한다면 `itemDetails` 합계와 같아야 합니다. 다르면 요청이 거부됩니다. **세금.** 이 빠른 시작 가이드와 예제 페이로드는 Tokenz 계정의 기본 설정인 Merchant of Record(MoR) 모델을 설명합니다. 이 모델에서는 Tokenz가 세금을 자동으로 계산하고 징수하므로 각 항목의 `taxCategory`만 지정하면 됩니다. 계정이 별도로 Payment Gateway 모델로 설정되어 있다면 모든 Checkout Session에 최소 단위의 최상위 `tax` 객체도 포함해야 합니다(예: `"tax": { "amount": 0, "currency": "USD" }`). 이를 생략하면 `order.tax-required` 오류로 요청이 거부됩니다. 계정에 적용된 모델이 확실하지 않으면 Tokenz 담당자에게 문의하세요. ### 성공 및 취소 URL 지정 성공 페이지와 취소 페이지의 URL을 지정하세요. Tokenz가 고객을 해당 페이지로 이동시킬 수 있도록 외부에서 접근할 수 있어야 합니다. 같은 URL에서 성공 상태와 취소 상태를 모두 처리해도 됩니다. ### [선택] 사용 가능한 결제 수단 지정 이번 결제에서 고객에게 제공할 결제 수단 목록을 지정하세요. 생략하거나 빈 목록을 전송하면 사용 가능한 모든 결제 수단이 표시됩니다. ### 고객을 Tokenz Checkout으로 이동 세션을 생성한 후 응답에 반환된 결제 페이지 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); }); }); ``` ## 결제 화면 구성 ### 성공 페이지 추가 Checkout Session의 `successUrl`에 지정한 URL에 성공 페이지를 만들어 주문 확인 메시지나 주문 정보를 표시하세요. 고객이 결제를 완료하면 Tokenz가 이 페이지로 이동시킵니다. ```htmlWe appreciate your business! If you have any questions, please email orders@example.com.
Your payment is being processed! We will let you know as soon as it is completed. If you have any questions, please email orders@example.com.
Forgot to add something to your cart? Shop around then come back to pay!