curl --request GET 'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state'
const input = {
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
redirect_uri: 'https://assistant.example.com/oauth/callback',
response_type: 'code',
response_mode: 'query',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
resource: 'https://api.voicecheap.ai/mcp',
scope: 'account:read projects:read offline_access',
state: 'example-client-csrf-state',
};
const response = await fetch(
'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state',
{
method: 'GET',
headers: {},
redirect: 'manual',
},
);
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"GET",
"https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state",
headers={},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
OAuth 및 MCP 프로토콜
MCP 계정 권한 부여 시작
수명이 짧은 권한 부여 요청을 만들고 사용자를 VoiceCheap 로그인 및 권한 동의 페이지로 리디렉션합니다.
GET
/
oauth
/
authorize
curl --request GET 'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state'
const input = {
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
redirect_uri: 'https://assistant.example.com/oauth/callback',
response_type: 'code',
response_mode: 'query',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
resource: 'https://api.voicecheap.ai/mcp',
scope: 'account:read projects:read offline_access',
state: 'example-client-csrf-state',
};
const response = await fetch(
'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state',
{
method: 'GET',
headers: {},
redirect: 'manual',
},
);
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"GET",
"https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state",
headers={},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
수명이 짧은 권한 부여 요청을 만들고 사용자를 VoiceCheap 로그인 및 권한 동의 페이지로 리디렉션합니다.
GET https://api.voicecheap.ai/oauth/authorize
인증
이 경로에서는 없습니다. 이후 브라우저 동의 흐름에서 사용자 로그인이 필요하며, 승인하려면 자격을 갖춘 유료 계정 접근 권한이 필요합니다.쿼리 매개변수
string
필수
동적 등록에서 반환한 client_id 또는 클라이언트 메타데이터 문서의 정확한 공개 HTTPS URL입니다.최소 길이: 1. 최대 길이: 2048. 패턴:
^[^\u0000-\u001F\u007F]+$.string
필수
이 클라이언트에 등록된 redirect_uris 중 하나와 정확히 일치해야 합니다.최소 길이: 1. 최대 길이: 2048. 패턴:
^[^\u0000-\u001F\u007F]+$.field
필수
전체 입력 스키마에서 정의한 값입니다.필수 값: “code”.
field
전체 입력 스키마에서 정의한 값입니다.필수 값: “query”. 기본값: “query”.
string
필수
패딩 없는 원본 code_verifier의 Base64url SHA-256 다이제스트입니다.최소 길이: 43. 최대 길이: 43. 패턴:
^[A-Za-z0-9_-]{43}$.field
필수
전체 입력 스키마에서 정의한 값입니다.필수 값: “S256”.
string
필수
필수 리소스 대상 값은 정확히 일치해야 합니다. 이 환경의 보호된 리소스 메타데이터에 있는 resource 값을 사용하세요. 프로덕션에서는 https://api.voicecheap.ai/mcp입니다. 다른 오리진, 경로 또는 끝의 슬래시는 허용되지 않습니다.형식: “uri”.
string
scopes_supported의 권한을 공백으로 구분하며 선택적으로 offline_access를 포함할 수 있습니다. 지원되지 않는 이름과 공백만 있는 값은 거부되며 중복은 제거됩니다. scope를 생략하거나 openid만 또는 offline_access만 지정하는 등 제품 권한을 제공하지 않으면 기본 읽기 범위와 이 클라이언트에 등록된 제품 범위의 교집합을 선택합니다. email에는 openid가 필요하며 email만 지정하면 거부됩니다. 요청한 제품 권한은 이 클라이언트에 등록되어 있어야 합니다. 신원 권한은 명시적으로 요청하고 사용자가 승인해야 하며 scope를 생략하면 신원 권한을 요청하지 않습니다.최소 길이: 1. 최대 길이: 2048. 패턴:
^ *(?:account:read|usage:read|settings:read|settings:write|team:read|team:write|projects:read|projects:write|projects:delete|translations:write|transcription:read|transcription:write|subtitles:read|subtitles:write|tts:read|tts:write|voices:read|voices:write|images:read|images:write|thumbnails:read|thumbnails:write|folders:read|folders:write|glossary:read|glossary:write|webhooks:read|webhooks:write|publishing:read|publishing:write|offline_access|openid|email)(?: +(?:account:read|usage:read|settings:read|settings:write|team:read|team:write|projects:read|projects:write|projects:delete|translations:write|transcription:read|transcription:write|subtitles:read|subtitles:write|tts:read|tts:write|voices:read|voices:write|images:read|images:write|thumbnails:read|thumbnails:write|folders:read|folders:write|glossary:read|glossary:write|webhooks:read|webhooks:write|publishing:read|publishing:write|offline_access|openid|email))* *$.string
최종 콜백에서 그대로 반환되는 선택적 불투명 값입니다. 매번 새로운 예측 불가능한 값을 사용하고 클라이언트에서 검증하세요.최소 길이: 1. 최대 길이: 2048. 패턴:
^[^\u0000-\u001F\u007F]+$.string
openid가 부여되면 ID 토큰에 그대로 반환되는 선택적 불투명 nonce 값입니다. 클라이언트에서 검증하세요.최소 길이: 1. 최대 길이: 2048. 패턴:
^[^\u0000-\u001F\u007F]+$.string
텍스트 값입니다.필수 값: “consent”.
기술 스키마
기술 스키마
{
"type": "object",
"properties": {
"client_id": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[^\\u0000-\\u001F\\u007F]+$",
"description": "The client_id returned by dynamic registration, or the exact public HTTPS URL of the client metadata document."
},
"redirect_uri": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[^\\u0000-\\u001F\\u007F]+$",
"description": "Must exactly match one of this client's registered redirect_uris."
},
"response_type": {
"const": "code"
},
"response_mode": {
"const": "query",
"default": "query"
},
"code_challenge": {
"type": "string",
"minLength": 43,
"maxLength": 43,
"pattern": "^[A-Za-z0-9_-]{43}$",
"description": "Base64url SHA-256 digest of the original code_verifier, without padding."
},
"code_challenge_method": {
"const": "S256"
},
"resource": {
"type": "string",
"format": "uri",
"description": "Required exact resource audience: use the resource value from this environment's protected-resource metadata. In production this is https://api.voicecheap.ai/mcp. A different origin, path, or trailing slash is rejected.",
"examples": [
"https://api.voicecheap.ai/mcp"
]
},
"scope": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^ *(?:account:read|usage:read|settings:read|settings:write|team:read|team:write|projects:read|projects:write|projects:delete|translations:write|transcription:read|transcription:write|subtitles:read|subtitles:write|tts:read|tts:write|voices:read|voices:write|images:read|images:write|thumbnails:read|thumbnails:write|folders:read|folders:write|glossary:read|glossary:write|webhooks:read|webhooks:write|publishing:read|publishing:write|offline_access|openid|email)(?: +(?:account:read|usage:read|settings:read|settings:write|team:read|team:write|projects:read|projects:write|projects:delete|translations:write|transcription:read|transcription:write|subtitles:read|subtitles:write|tts:read|tts:write|voices:read|voices:write|images:read|images:write|thumbnails:read|thumbnails:write|folders:read|folders:write|glossary:read|glossary:write|webhooks:read|webhooks:write|publishing:read|publishing:write|offline_access|openid|email))* *$",
"description": "Space-delimited permissions from scopes_supported, optionally including offline_access. Unsupported names and whitespace-only values are rejected; duplicates are removed. Omitting scope or supplying no product scopes, such as openid alone or offline_access alone, selects the default read scopes intersected with this client's registered product scopes. email requires openid; email alone is rejected. Requested product permissions must be registered for this client. Identity permissions must be explicitly requested and approved by the user; omitting scope requests no identity permissions.",
"examples": [
"account:read projects:read offline_access"
]
},
"state": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[^\\u0000-\\u001F\\u007F]+$",
"description": "Optional opaque value echoed on the final callback. Use a fresh unpredictable value and verify it in the client."
},
"nonce": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[^\\u0000-\\u001F\\u007F]+$",
"description": "Optional opaque nonce echoed in the ID token when openid is granted. Verify it in the client."
},
"prompt": {
"type": "string",
"const": "consent"
}
},
"required": [
"client_id",
"redirect_uri",
"response_type",
"code_challenge",
"code_challenge_method",
"resource"
],
"additionalProperties": true
}
요청 예제
이 경로에 명시된 인코딩 및 인증 방식을 따르세요. 액션 API의 응답 구조와 멱등성 규칙은 액션 호출에만 적용됩니다.curl --request GET 'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state'
const input = {
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
redirect_uri: 'https://assistant.example.com/oauth/callback',
response_type: 'code',
response_mode: 'query',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
resource: 'https://api.voicecheap.ai/mcp',
scope: 'account:read projects:read offline_access',
state: 'example-client-csrf-state',
};
const response = await fetch(
'https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state',
{
method: 'GET',
headers: {},
redirect: 'manual',
},
);
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"GET",
"https://api.voicecheap.ai/oauth/authorize?client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&redirect_uri=https%3A%2F%2Fassistant.example.com%2Foauth%2Fcallback&response_type=code&response_mode=query&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fapi.voicecheap.ai%2Fmcp&scope=account%3Aread+projects%3Aread+offline_access&state=example-client-csrf-state",
headers={},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
응답
VoiceCheap 동의 페이지로 HTTP 리디렉션합니다. JSON 결과 본문은 없습니다. Location에는 불투명한 request 값이 포함됩니다. 이후 사용자가 로그인하여 권한을 선택합니다.HTTP/1.1 302 Found
Cache-Control: no-store
Pragma: no-cache
Referrer-Policy: no-referrer
Location: https://app.voicecheap.ai/mcp/authorize?request=mcp_req_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
응답 헤더
{
"Cache-Control": "no-store",
"Pragma": "no-cache",
"Referrer-Policy": "no-referrer",
"Location": "https://app.voicecheap.ai/mcp/authorize?request=mcp_req_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
}
작업 참고 사항
- 인식되는 스칼라 매개변수는 ASCII 제어 문자나 DEL이 없는 비어 있지 않은 문자열로 한 번만 제공해야 합니다. 반복된 쿼리/폼 필드는 배열로 디코딩되며, 스칼라로 읽을 때 거부됩니다.
- 모든 쿼리 값을 URL 인코딩하세요. redirect_uri는 이 클라이언트에 등록된 표기를 정확히 사용해야 합니다.
- S256만 사용하세요. plain PKCE 및 implicit/token 응답은 지원되지 않습니다. 토큰 교환에 사용할 원래의 43–128자 code_verifier를 보관하세요.
- resource 매개변수는 필수이며 선택한 환경의 MCP 리소스 URL과 정확히 일치해야 합니다.
- 기본 제품 범위 집합은 account:read usage:read projects:read transcription:read subtitles:read tts:read voices:read images:read thumbnails:read입니다. 기본값이 적용된 요청은 클라이언트가 등록한 범위로 축소되며, 제품 범위가 하나도 남지 않으면 인증이 실패합니다.
- offline_access만 있는 범위는 기본 제품 범위 선택을 사용합니다. 제품 권한을 표시하거나 부여하기 전에 offline_access는 제거됩니다.
- 동의 요청은 10분 후에 만료되며 한 번만 응답할 수 있습니다. 사용자는 요청된 제품 권한 중 비어 있지 않은 하위 집합만 승인할 수 있습니다.
- 승인 후 클라이언트 콜백은 code, iss 및 제공된 경우 원래 state를 받습니다. 인증 코드는 2분 후 만료되며 한 번만 교환할 수 있습니다.
- 거부 후 클라이언트 콜백에는 error=access_denied, error_description, iss 및 원래 제공된 state가 전달되며 코드는 발급되지 않습니다.
- 잘못된 클라이언트 메타데이터, 리디렉션 불일치, PKCE, scopes 또는 resource는 OAuth JSON 오류를 직접 반환하며, 검증되지 않은 콜백으로 리디렉션되지 않습니다.
- 공개 인증 엔드포인트의 한도는 클라이언트 IP당 분당 30회 요청이며 워커 간에 공유됩니다.
- openid와 email은 제품 권한과 별개인 신원 권한이며, 지원이 명시된 경우에만 허용됩니다. email에는 openid가 필요합니다. 최초 권한 부여 시 신원 범위만 요청하면 동의 화면에 표시되는 기본 제품 권한이 사용됩니다.
- 동적 등록에서 반환한 client_id 또는 클라이언트 메타데이터 문서의 정확한 공개 HTTPS URL입니다.
- OpenID Connect는 prompt=consent를 지원합니다. 다른 prompt 값과 max_age는 거부됩니다. 자동 승인이나 강제 재로그인은 지원되지 않습니다.
오류
| HTTP 상태 | 코드 | 의미 또는 복구 방법 |
|---|---|---|
| 400 | invalid_request | 본문 또는 인식된 매개변수가 유효하지 않거나, 누락되었거나, 중복되었거나, 인코딩이 잘못되었습니다. |
| 400 | invalid_client | client_id를 알 수 없거나 클라이언트 메타데이터 문서를 가져오거나 검증할 수 없습니다. |
| 400 | unsupported_response_type | response_type이 code가 아닙니다. |
| 400 | invalid_target | resource가 없거나 환경의 MCP URL과 정확히 일치하지 않습니다. |
| 400 | invalid_scope | 요청한 제품 범위가 지원되지 않거나 등록되지 않았거나, 사용 가능한 권한이 남지 않습니다. |
| 429 | temporarily_unavailable | 공개 OAuth 요청 속도 제한에 도달했습니다. 현재 제한 시간 구간이 끝날 때까지 기다린 후 다시 시도하세요. |
| 500 | server_error | 일시적으로 권한 부여를 사용할 수 없습니다. 나중에 다시 시도하세요. |
| 503 | temporarily_unavailable | 클라이언트 메타데이터를 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요. |
이 페이지가 도움이 되었나요?

