본문으로 건너뛰기

테넌트를 Pro 플랜에 구독시키기

Console을 열지 않고도 Logto Cloud API 호출 한 번으로 테넌트를 Pro 플랜에 올릴 수 있습니다. 테넌트 생성과 함께 사용하면, 자동화가 프로덕션 테넌트를 만들고 구독시키는 작업을 두 번의 호출로 처리할 수 있습니다.

이 호출은 청구 계정에 저장된 카드로 첫 번째 인보이스를 결제합니다. 결제 확인을 위해 사람이 개입하지 않으므로, 호출은 완전히 성공하거나 아무것도 남기지 않습니다: 결제 실패 시 구독이 생성되지 않습니다.

시작하기 전에​

다음이 필요합니다:

  • 이 API에 접근할 수 있는 Logto Cloud Personal Access Token (PAT). 발급을 원하시면 저희에게 문의하세요.
  • 테넌트의 Admin 역할. 테넌트를 생성한 사용자가 Admin입니다.
  • 카드가 저장된 청구 계정. Logto Cloud 계정은 Console > 설정 > 플랜 및 결제에서 처음으로 어떤 테넌트든 Pro 플랜에 구독시키면 청구 계정이 생성됩니다. API는 기본 청구 계정의 카드로 결제합니다. 단, 테넌트가 이전에 유료 플랜을 사용한 적이 있거나 다른 계정을 선택한 경우는 예외입니다.
  • Free 플랜의 프로덕션 테넌트. 개발 테넌트 및 엔터프라이즈 계약이 적용된 테넌트는 지원하지 않습니다.
VariableDescription
CLOUD_API_ENDPOINTLogto Cloud API 엔드포인트. Logto Cloud의 경우 https://cloud.logto.io를 사용하세요.
LOGTO_CLOUD_PATLogto 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" }'
  1. Idempotency-Key: 필수. 이 시도를 식별하는 1~255자의 고유 문자열입니다. 시도마다 하나씩 생성해 저장하고, 재시도 시 동일한 값을 보내세요.
  2. skuId: 필수. 구독할 플랜입니다. Pro 플랜은 pro-202509를 사용하세요.
  3. 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 계정은 여러 개의 청구 계정을 가질 수 있으며, 각각 카드와 청구 주소가 다를 수 있습니다. 예를 들어, 결제하는 회사별로 하나씩 둘 수 있습니다. 이 중 하나가 기본값이며, 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는 키당 한 번만 결제하므로 동일한 키로 재시도하는 것은 항상 안전합니다.

  1. 결과를 알 수 없을 때는 동일한 키로 재시도하세요. 클라이언트 타임아웃, 연결 끊김, 5xx 응답, 409 attempt_in_progress(이 키로 보낸 이전 요청이 아직 처리 중일 수 있으니 잠시 기다렸다가 재시도) 모두 해당됩니다.
  2. 재시도가 시도를 확정합니다. 구독이 존재하면 201 또는 200을 반환하거나, 시도를 종료시킨 에러를 반환합니다.
  3. 시도가 에러로 끝난 경우에만 새 키로 새 시도를 시작하세요. 동일한 키로 재시도하면 이미 실패했다는 메시지가 반환됩니다. 원인을 먼저 해결하세요(예: 카드 정보 갱신).
  4. 메시지에서 문의하라고 하면 중단하고 저희에게 연락하세요. 응답에 error.requestId가 있으면 함께 보내주세요.
노트:

시도가 열려 있는 동안, 해당 테넌트는 예약됩니다: 다른 키로 요청하면 시도가 끝날 때까지 409 attempt_in_progress가 반환됩니다. 새로 시작하지 말고 기존 키로 재시도하세요. 키는 계정 단위로 범위가 지정됩니다.

23시간 이내에 재시도하세요. 그 이후에는 Logto가 단일 결제를 보장할 수 없으므로 시도를 보류합니다: 동일한 키로 재시도하면 409 attempt_in_progress와 함께 지원팀에 문의하라는 메시지가 반환되며, 저희가 수동으로 처리합니다.

오류​

오류는 HTTP 상태와 message, 대부분의 경우 error.code가 포함된 JSON 본문을 사용합니다:

{
"message": "카드가 거절되었습니다.",
"error": { "code": "card_declined", "declineCode": "insufficient_funds" }
}
Statuserror.code의미 및 조치
400(없음)Idempotency-Key 헤더가 없거나 255자를 초과했거나, 본문에 skuId가 없습니다.
400invalid_sku해당 skuId는 API로 구매할 수 없습니다.
400idempotency_key_mismatch이 키가 이미 다른 테넌트, 플랜 또는 청구 계정에 사용되었습니다. 메시지에서 지원팀 문의를 요청하지 않는 한 새 키를 사용하세요.
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 또는 없음내부 오류가 발생했습니다. 동일한 키로 재시도하고, 반복되면 저희에게 문의하세요.
502stripe_error결제 제공자가 요청을 거부하거나 실패했습니다. 동일한 키로 재시도하고, 반복되면 저희에게 문의하세요.
503stripe_unavailable, provisioning_failed결과를 알 수 없거나, 결제는 성공했으나 설정이 아직 완료되지 않았습니다. 동일한 키로 재시도하세요.

동일한 청구 계정을 사용하는 테넌트의 Console > 설정 > 플랜 및 결제에서 카드와 청구 주소를 갱신할 수 있습니다.

한 스크립트로 생성 및 구독하기​

테넌트 생성에는 idempotency key가 없습니다. POST /api/tenants가 타임아웃되면, 다시 생성하기 전에 GET /api/tenants로 테넌트 목록을 확인해 중복 생성을 방지하세요.

import { randomUUID } from 'node:crypto';

// 환경 변수에서 API 엔드포인트와 PAT를 가져옵니다.
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}`
);
}