テナントを Pro プランにサブスクライブする
Logto Cloud API の 1 回の呼び出しで、Console を開かずにテナントを Pro プランに移行できます。テナント作成 と組み合わせることで、自動化スクリプトから本番テナントの作成とサブスクライブを 2 回の呼び出しで実現できます。
この呼び出しは、請求アカウントに保存されたカードに最初の請求書を課金します。支払いの確認者は存在しないため、呼び出しは完全に成功するか、何も残しません:支払いに失敗した場合、サブスクリプションは作成されません。
始める前に
必要なもの:
- この API へのアクセス権を持つ Logto Cloud パーソナルアクセストークン (PAT)。詳しくは PAT の作成 を参照してください。
- テナントの Admin ロール。テナントを作成したユーザーが Admin です。
- 保存済みカード付きの請求アカウント。Logto Cloud アカウントは、Console > 設定 > プランと請求 で初めてテナントを Pro プランにサブスクライブした際に 1 つ作成されます。API はデフォルトの請求アカウントのカードに課金しますが、テナントが以前に有料プランだった場合や 別のアカウントを選択 した場合はその限りではありません。
- Free プランの 本番テナント。開発テナントやエンタープライズ契約対象のテナントはサポートされていません。
| Variable | Description |
|---|---|
CLOUD_API_ENDPOINT | Logto Cloud API エンドポイント。Logto Cloud の場合は https://cloud.logto.io を使用します。 |
LOGTO_CLOUD_PAT | Logto Cloud アカウント用の PAT。 |
TENANT_ID | サブスクライブするテナントの ID。 |
テナントをサブスクライブする
Idempotency-Key ヘッダー付きで POST /api/tenants/{tenantId}/subscription を呼び出します:
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:このキーですでにサブスクリプションが作成されています。再度課金はされません。
レスポンス例:
{
"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 アカウントは複数の請求アカウントを持つことができ、それぞれにカードや請求先住所を設定できます(例:会社ごとに 1 つ)。そのうち 1 つがデフォルトで、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 の場合もあります。既に存在しない請求アカウントは表示されません。
特定の請求アカウントで 1 件のサブスクリプションに課金するには、リクエストボディにその 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 はキーごとに 1 回しか課金しないため、同じキーでのリトライは常に安全 です。
- 結果が不明な場合は同じキーでリトライしてください。 これはクライアントタイムアウトや接続切断、
5xxレスポンス、409attempt_in_progress(このキーのリクエストがまだ進行中の場合は数秒待ってから)に対応します。 - リトライで試行が確定します。 サブスクリプションが存在すれば
201または200、または試行を終了させたエラーが返されます。 - 試行がエラーで終了した場合のみ新しいキーで新しい試行を開始してください。 同じキーでのリトライは「既に失敗した」と返されます。まず原因を修正してください(例:カード情報の更新)。
- メッセージで問い合わせを求められた場合は、指示に従いご連絡ください。 レスポンスに
error.requestIdが含まれていれば一緒にお知らせください。
試行がオープンな間は、そのテナントは予約状態です:異なるキーでのリクエストは 409 attempt_in_progress となります。新しい試行を始めるのではなく、元のキーでリトライしてください。キーはアカウント単位でスコープされます。
23 時間以内にリトライしてください。それを過ぎると Logto は単一課金を保証できなくなり、試行を保留します:同じキーでのリトライは 409 attempt_in_progress とサポートへの連絡を促すメッセージを返し、手動で対応します。
エラー
エラーは HTTP ステータスと、message および多くの場合 error.code を含む JSON ボディで返されます:
{
"message": "The card was declined.",
"error": { "code": "card_declined", "declineCode": "insufficient_funds" }
}
| Status | error.code | 意味と対応方法 |
|---|---|---|
400 | (なし) | Idempotency-Key ヘッダーがない、255 文字を超えている、またはボディに skuId がない場合。 |
400 | invalid_sku | skuId は API から購入できません。 |
400 | idempotency_key_mismatch | このキーは他のテナント・プラン・請求アカウントで既に使用されています。サポートからの指示がない限り新しいキーを使ってください。 |
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 またはなし | サーバー側で問題が発生しました。同じキーでリトライし、繰り返す場合はお問い合わせください。 |
502 | stripe_error | 決済プロバイダーがリクエストを拒否または失敗しました。同じキーでリトライし、繰り返す場合はお問い合わせください。 |
503 | stripe_unavailable, provisioning_failed | 結果が不明、または支払いは成功しセットアップが未完了です。同じキーでリトライしてください。 |
同じ請求アカウントを使う任意のテナントの Console > 設定 > プランと請求 でカードや請求先住所を更新できます。
1 つのスクリプトで作成とサブスクライブ
テナント作成には冪等性キーがありません。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(`テナント作成に失敗しました: ${await created.text()}`);
}
const tenant = await created.json();
// キーをテナントと一緒に保存し、後で同じ試行をリトライできるようにします
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)) {
// 安全:同じキーで二重課金されることはありません
await new Promise((resolve) => setTimeout(resolve, 2000 * (attempt + 1)));
} else {
throw new Error(`サブスクリプションが拒否されました: ${await response.text()}`);
}
}
if (!subscription) {
throw new Error(`まだ不明です。同じキーで後ほどリトライしてください: ${idempotencyKey}`);
}