跳至主要內容

訂閱租戶至 Pro 方案

你可以只需一次 Logto Cloud API 呼叫,將租戶升級至 Pro 方案,無需開啟 Console。結合租戶建立,你的自動化流程只需兩次呼叫即可建立生產租戶並訂閱。

此呼叫會將第一張發票收費至你帳戶中已儲存的信用卡。由於無人確認付款,呼叫要麼完全成功,要麼完全不會留下任何紀錄:付款失敗時不會建立訂閱。

開始前準備​

你需要:

  • 一組 Logto Cloud Personal Access Token (PAT),且有權存取此 API。請聯絡我們取得。
  • 在該租戶上的 Admin 角色 (Admin)。建立租戶的使用者即為其 Admin。
  • 一個已儲存信用卡的帳單帳戶 (billing account with a saved card)。你的 Logto Cloud 帳戶在首次於 Console > 設定 > 方案與帳單 訂閱任一租戶至 Pro 方案時會自動建立。API 會收費至你的預設帳單帳戶,除非該租戶曾經訂閱過付費方案,或你選擇其他帳戶。
  • 一個Free 方案的生產租戶 (production tenant)。不支援開發租戶與企業合約涵蓋的租戶。
VariableDescription
CLOUD_API_ENDPOINTLogto Cloud API 端點。Logto Cloud 請使用 https://cloud.logto.io。
LOGTO_CLOUD_PAT你的 Logto Cloud 帳戶 PAT。
TENANT_ID要訂閱的租戶 ID。

訂閱租戶​

呼叫 POST /api/tenants/{tenantId}/subscription,並加上 Idempotency-Key 標頭:

export IDEMPOTENCY_KEY="$(uuidgen)"

curl -X POST "$CLOUD_API_ENDPOINT/api/tenants/$TENANT_ID/subscription" \
-H "Authorization: Bearer $LOGTO_CLOUD_PAT" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{ "skuId": "pro-202509" }'
  1. Idempotency-Key:必填。1 至 255 字元的唯一字串,用於識別本次操作。每次嘗試都產生一組,並儲存,重試時傳送相同值。
  2. skuId:必填。要訂閱的方案。Pro 方案請使用 pro-202509。
  3. customerId:選填。要收費的帳單帳戶,當你有多個帳戶時使用。若省略,曾經訂閱過付費方案的租戶會收費至其原帳戶;其他租戶則收費至你的預設帳戶。詳見選擇帳單帳戶。

回應狀態碼說明結果:

  • 201:已建立訂閱,租戶已升級至 Pro 方案。
  • 200:此 key 已建立過訂閱,未再次收費。

範例回應:

{
"subscription": {
"id": "sub_1Qabc...",
"planId": "pro-202509",
"status": "active",
"currentPeriodStart": "2026-09-20T06:00:00.000Z",
"currentPeriodEnd": "2026-10-20T06:00:00.000Z",
"isEnterprisePlan": false,
"isDevPlan": false,
"quotaScope": "dedicated"
},
"tenant": {
"id": "abc123",
"tag": "production",
"planId": "pro-202509"
}
}

選擇帳單帳戶​

你的 Logto Cloud 帳戶可擁有多個帳單帳戶,每個帳戶有自己的信用卡與帳單地址,例如每家公司一個。預設帳戶會被 API 收費,除非你指定其他帳戶。

使用 GET /api/me/stripe-customers 列出你的帳單帳戶:

curl "$CLOUD_API_ENDPOINT/api/me/stripe-customers" \
-H "Authorization: Bearer $LOGTO_CLOUD_PAT"
[
{
"customerId": "cus_Rabc...",
"isDefault": true,
"name": "Acme Inc.",
"email": "[email protected]",
"createdAt": "2026-03-02T09:12:45.000Z"
},
{
"customerId": "cus_Rdef...",
"isDefault": false,
"name": null,
"email": "[email protected]",
"createdAt": "2026-08-14T15:30:00.000Z"
}
]

name 與 email 為帳單帳戶上儲存的資訊,可能為 null。已不存在的帳戶不會顯示。

若要指定某個帳戶收費,請在請求內容中傳送其 customerId:

curl -X POST "$CLOUD_API_ENDPOINT/api/tenants/$TENANT_ID/subscription" \
-H "Authorization: Bearer $LOGTO_CLOUD_PAT" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{ "skuId": "pro-202509", "customerId": "cus_Rdef..." }'

若要更改預設收費帳戶(API 與 Console 均適用,注意:這不會移動曾經訂閱過付費方案的租戶,詳見下方說明):

curl -X PATCH "$CLOUD_API_ENDPOINT/api/me/stripe-customers/cus_Rdef..." \
-H "Authorization: Bearer $LOGTO_CLOUD_PAT" \
-H "Content-Type: application/json" \
-d '{ "isDefault": true }'

成功回應 204,若帳戶不存在則回應 404,若帳戶已失效則回應 422 customer_unavailable。

備註:

曾經訂閱過付費方案的租戶,即使該訂閱已取消,仍綁定於原付款帳戶。此類租戶始終收費至該帳戶,無論預設帳戶為何。請勿填寫 customerId,若填寫則回應 422 customer_fixed。如需移動租戶至其他帳戶,請聯絡我們。

同一 Idempotency-Key 重試時,必須指定與首次請求相同的帳戶,否則回應 400 idempotency_key_mismatch。

安全重試​

網路逾時時無法得知是否已收費。Idempotency-Key 可確保重試安全:Logto 每個 key 最多只收費一次,因此用相同 key 重試永遠安全。

  1. 結果未知時,請用相同 key 重試。 包含客戶端逾時、連線中斷、5xx 回應,以及 409 attempt_in_progress(此 key 的前次請求可能仍在處理,請先等幾秒)。
  2. 重試會結束本次操作。 訂閱建立後會回應 201 或 200,或回傳導致失敗的錯誤。
  3. 僅在操作已失敗時,才用新 key 開始新嘗試。 若用相同 key 重試,會收到操作已失敗的訊息。請先排除原因,例如更新信用卡。
  4. 如訊息要求,請停止並聯絡我們。 若回應有 error.requestId,請一併提供。
備註:

操作進行中時,該租戶會被保留:不同 key 的請求會收到 409 attempt_in_progress,直到操作結束。請用原 key 重試,不要用新 key。key 以你的帳戶為範圍。

請於 23 小時內重試。超過後,Logto 無法保證只收費一次,並會暫停操作:同 key 重試會收到 409 attempt_in_progress 並提示聯絡支援,我們會人工協助處理。

錯誤說明​

錯誤回應包含 HTTP 狀態碼與 JSON 內容,內含 message 及多數錯誤的 error.code:

{
"message": "The card was declined.",
"error": { "code": "card_declined", "declineCode": "insufficient_funds" }
}
Statuserror.code意義與處理方式
400(none)缺少 Idempotency-Key 標頭、長度超過 255 字元,或內容缺少 skuId。
400invalid_sku此 skuId 無法透過 API 購買。
400idempotency_key_mismatch此 key 已用於其他租戶、方案或帳戶。請用新 key,除非訊息要求聯絡支援。
402card_declined, expired_card, incorrect_cvc, incorrect_number信用卡扣款失敗。若發卡行有提供,declineCode 會顯示原因。請於 Console 更新信用卡後重新嘗試。
402processing_error此次無法處理信用卡。稍後重新嘗試。
402authentication_required發卡行要求持卡人確認付款,API 無法完成。請於 Console 訂閱此租戶。
403insufficient_role你是租戶成員但不是 Admin。
403(none)權杖無法存取此 API,或租戶受企業合約涵蓋、或位於私人區域。
404(none)租戶不存在,或你不是其成員。
404customer_not_foundcustomerId 不是你的帳單帳戶。請用 GET /api/me/stripe-customers 查詢。
409subscription_exists租戶已有訂閱。
409attempt_in_progress此租戶有進行中的操作,或本次操作被暫停。詳見安全重試。
409no_customer, no_payment_method你的帳戶沒有帳單帳戶,或帳戶未儲存信用卡。請先於 Console 訂閱一次租戶,或新增信用卡。
409tax_location_invalid帳單地址無法計算稅金。請於 Console 更新。
409customer_not_owned, customer_unavailable, currency_mismatch, attempt_abandoned租戶的帳單帳戶無法驗證為你的,或需我們協助。請聯絡我們。
422customer_fixed租戶綁定於先前付款帳戶。請勿填寫 customerId,或聯絡我們協助移動租戶。
422dev_tenant_not_supported開發租戶無法透過 API 訂閱。請於 Console 轉換租戶。
422subscription_status_exception租戶最新訂閱需先處理。請聯絡我們。
500internal_error 或無伺服器端發生錯誤。請用相同 key 重試,若重複發生請聯絡我們。
502stripe_error金流服務商拒絕或處理失敗。請用相同 key 重試,若重複發生請聯絡我們。
503stripe_unavailable, provisioning_failed結果未知,或付款成功但尚未完成設定。請用相同 key 重試。

你可於任何使用相同帳單帳戶的租戶 Console > 設定 > 方案與帳單 更新信用卡與帳單地址。

一鍵建立並訂閱腳本​

租戶建立不支援 idempotency key。若 POST /api/tenants 逾時,請先用 GET /api/tenants 查詢租戶,避免重試時建立重複租戶。

import { randomUUID } from 'node:crypto';

// 設定 API 端點與標頭
const cloudApiEndpoint = process.env.CLOUD_API_ENDPOINT ?? 'https://cloud.logto.io';
const headers = {
authorization: `Bearer ${process.env.LOGTO_CLOUD_PAT}`,
'content-type': 'application/json',
};

// 建立租戶
const created = await fetch(`${cloudApiEndpoint}/api/tenants`, {
method: 'POST',
headers,
body: JSON.stringify({ name: 'My automated tenant', tag: 'production', regionName: 'EU' }),
});

if (!created.ok) {
throw new Error(`Tenant creation failed: ${await created.text()}`);
}

const tenant = await created.json();

// 將 key 與租戶一同儲存,方便後續重試
const idempotencyKey = randomUUID();

const subscribe = async () =>
fetch(`${cloudApiEndpoint}/api/tenants/${tenant.id}/subscription`, {
method: 'POST',
headers: { ...headers, 'idempotency-key': idempotencyKey },
body: JSON.stringify({ skuId: 'pro-202509' }),
});

const isOutcomeUnknown = async (response) =>
!response ||
response.status >= 500 ||
(response.status === 409 &&
(await response.clone().json()).error?.code === 'attempt_in_progress');

let subscription;

for (let attempt = 0; attempt < 5 && !subscription; attempt += 1) {
const response = await subscribe().catch(() => undefined);

if (response?.ok) {
subscription = await response.json();
} else if (await isOutcomeUnknown(response)) {
// 安全:相同 key 不會重複收費
await new Promise((resolve) => setTimeout(resolve, 2000 * (attempt + 1)));
} else {
throw new Error(`Subscription refused: ${await response.text()}`);
}
}

if (!subscription) {
throw new Error(`仍未知結果。請稍後用相同 key 重試:${idempotencyKey}`);
}