跳至主要內容

訂閱租戶至 Pro 方案

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

此呼叫會將第一張發票收費至你帳戶綁定的信用卡。由於無人確認付款,呼叫要麼完全成功,要麼什麼都不會留下:付款失敗時不會建立訂閱。

開始前準備​

你需要:

  • 一組Logto Cloud Personal Access Token (PAT),需有權存取此 API。請參閱建立 PAT。
  • 該租戶的 Admin 角色 (Admin)。建立租戶的使用者即為其 Admin。
  • 一個已綁定信用卡的帳單帳戶 (billing account with a saved card)。首次將任何租戶訂閱至 Pro 方案時,你的 Logto Cloud 帳戶會在 Console > 設定 > 方案與帳單 建立一個帳單帳戶。API 會預設收費至你的預設帳單帳戶,除非該租戶曾經訂閱過付費方案,或你選擇其他帳戶。
  • 一個Free 方案下的生產租戶 (production tenant)。不支援開發租戶與企業合約涵蓋的租戶。
變數說明
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。已不存在的帳戶不會顯示。

若要指定某個帳單帳戶收費,請在請求 body 傳入其 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 body,包含 message 及多數情況下的 error.code:

{
"message": "The card was declined.",
"error": { "code": "card_declined", "declineCode": "insufficient_funds" }
}
狀態碼error.code意義與處理方式
400(無)缺少 Idempotency-Key 標頭、長度超過 255 字元,或 body 無 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(無)權杖無法存取此 API,或租戶受企業合約涵蓋、或位於私人區域。
404(無)租戶不存在,或你不是其成員。
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';

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();

// Store the key with the tenant, so a later run can retry the same attempt.
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)) {
// Safe: the same key never charges twice.
await new Promise((resolve) => setTimeout(resolve, 2000 * (attempt + 1)));
} else {
throw new Error(`Subscription refused: ${await response.text()}`);
}
}

if (!subscription) {
throw new Error(`Still unknown. Retry later with the same key: ${idempotencyKey}`);
}