訂閱租戶至 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_ENDPOINT | Logto 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" }'
Idempotency-Key:必填。1 到 255 字元的唯一字串,用於識別本次嘗試。每次嘗試產生一組,儲存後重試時傳送相同值。skuId:必填。要訂閱的方案。Pro 方案請用pro-202509。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.",
"createdAt": "2026-03-02T09:12:45.000Z"
},
{
"customerId": "cus_Rdef...",
"isDefault": false,
"name": null,
"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 重試永遠安全。
- 結果未知時,請用相同 key 重試。 包含客戶端逾時、連線中斷、
5xx回應,以及409attempt_in_progress(此 key 的前次請求可能仍在進行,請先等幾秒)。 - 重試會結束本次嘗試。 訂閱建立後會回應
201或200,或回傳結束本次嘗試的錯誤。 - 僅當嘗試已明確失敗時,才用新 key 開始新嘗試。 用相同 key 重試會收到「已失敗」訊息。請先排除原因,例如更新信用卡。
- 如訊息要求,請停止並聯絡我們。 若回應有
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。 |
400 | invalid_sku | 此 skuId 無法透過 API 購買。 |
400 | idempotency_key_mismatch | 此 key 已用於其他租戶、方案或帳單帳戶。請用新 key,除非訊息要求聯絡客服。 |
402 | card_declined, expired_card, incorrect_cvc, incorrect_number | 信用卡扣款失敗。若發卡行有提供,會有 declineCode。請在 Console 更新信用卡後重新嘗試。 |
402 | processing_error | 此次無法處理信用卡。稍後重新嘗試。 |
402 | authentication_required | 發卡行要求持卡人確認付款,API 無法完成。請在 Console 訂閱此租戶。 |
403 | insufficient_role | 你是租戶成員但不是 Admin。 |
403 | (無) | 權杖無法存取此 API,或租戶受企業合約涵蓋、或位於私人區域。 |
404 | (無) | 租戶不存在,或你不是其成員。 |
404 | customer_not_found | customerId 不是你的帳單帳戶之一。請用 GET /api/me/stripe-customers 查詢。 |
409 | subscription_exists | 租戶已有訂閱。 |
409 | attempt_in_progress | 此租戶有進行中的嘗試,或本次嘗試被暫停。詳見安全重試。 |
409 | no_customer, no_payment_method | 你的帳戶沒有帳單帳戶,或帳戶未綁定信用卡。請先在 Console 訂閱一次租戶,或新增信用卡。 |
409 | tax_location_invalid | 帳單地址無法用於計算稅金。請在 Console 更新。 |
409 | customer_not_owned, customer_unavailable, currency_mismatch, attempt_abandoned | 租戶的帳單帳戶無法驗證為你的,或需我們協助。請聯絡我們。 |
422 | customer_fixed | 租戶仍綁定原付款帳戶。請勿傳入 customerId,或聯絡我們協助移動租戶。 |
422 | dev_tenant_not_supported | 開發租戶無法透過 API 訂閱。請在 Console 轉換租戶。 |
422 | subscription_status_exception | 租戶最新訂閱需先處理。請聯絡我們。 |
500 | internal_error 或無 | 伺服器端錯誤。請用相同 key 重試,若重複發生請聯絡我們。 |
502 | stripe_error | 金流服務商拒絕或處理失敗。請用相同 key 重試,若重複發生請聯絡我們。 |
503 | stripe_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}`);
}