跳到主要内容

为租户订阅 Pro 计划

你可以通过一次 Logto Cloud API 调用将租户升级到 Pro 计划,无需打开 Console。结合租户创建,你的自动化流程只需两次调用即可创建生产租户并完成订阅。

该调用会将第一张账单计入你账单账户中保存的信用卡。由于没有人现场确认付款,因此调用要么完全成功,要么什么都不会留下:支付失败不会创建订阅。

开始前准备​

你需要:

  • 一个有权访问此 API 的 Logto Cloud 个人访问令牌 (PAT)。请联系我们获取。
  • 在租户上的 Admin 角色 (Role)。创建租户的用户即为其 Admin。
  • 一个已保存信用卡的账单账户。你的 Logto Cloud 账户在首次通过 Console > 设置 > 计划与账单 订阅任意租户 Pro 计划时会获得一个账单账户。API 会扣款自你的默认账单账户,除非该租户之前已在付费计划下,或你选择了其他账户。
  • 一个处于 Free 计划下的生产环境租户。不支持开发租户和已签企业合同的租户。
变量描述
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": "billing@acme.example",
"createdAt": "2026-03-02T09:12:45.000Z"
},
{
"customerId": "cus_Rdef...",
"isDefault": false,
"name": null,
"email": "finance@other.example",
"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 仅在你的账户下有效。

请在 23 小时内重试。超时后,Logto 无法再保证只扣一次款,会锁定该尝试:用相同 key 重试会返回 409 attempt_in_progress 并提示联系支持,我们会人工处理。

错误说明​

错误响应使用 HTTP 状态码和带有 message 及(大多数情况下)error.code 的 JSON:

{
"message": "The card was declined.",
"error": { "code": "card_declined", "declineCode": "insufficient_funds" }
}
状态码error.code含义及处理建议
400(无)缺少 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(无)令牌无权访问此 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 > 设置 > 计划与账单 中更新信用卡和账单地址。

一键创建并订阅​

租户创建没有幂等 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(`租户创建失败: ${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(`订阅被拒绝: ${await response.text()}`);
}
}

if (!subscription) {
throw new Error(`结果仍未知。请稍后用相同 key 重试: ${idempotencyKey}`);
}