curl --request POST 'https://api.voicecheap.ai/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \
--data-urlencode 'code=mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' \
--data-urlencode 'redirect_uri=https://assistant.example.com/oauth/callback' \
--data-urlencode 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
--data-urlencode 'resource=https://api.voicecheap.ai/mcp'
const input = {
grant_type: 'authorization_code',
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
code: 'mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
redirect_uri: 'https://assistant.example.com/oauth/callback',
code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
resource: 'https://api.voicecheap.ai/mcp',
};
const response = await fetch('https://api.voicecheap.ai/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(input),
redirect: 'manual',
});
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"POST",
"https://api.voicecheap.ai/oauth/token",
headers={"Content-Type":"application/x-www-form-urlencoded"},
data={"grant_type": "authorization_code", "client_id": "mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "code": "mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "redirect_uri": "https://assistant.example.com/oauth/callback", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "resource": "https://api.voicecheap.ai/mcp"},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
OAuth and MCP protocol
Exchange an authorization code or rotate a refresh token
Obtain short-lived MCP access tokens using a public client’s S256 authorization code or its current refresh token.
POST
/
oauth
/
token
curl --request POST 'https://api.voicecheap.ai/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \
--data-urlencode 'code=mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' \
--data-urlencode 'redirect_uri=https://assistant.example.com/oauth/callback' \
--data-urlencode 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
--data-urlencode 'resource=https://api.voicecheap.ai/mcp'
const input = {
grant_type: 'authorization_code',
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
code: 'mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
redirect_uri: 'https://assistant.example.com/oauth/callback',
code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
resource: 'https://api.voicecheap.ai/mcp',
};
const response = await fetch('https://api.voicecheap.ai/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(input),
redirect: 'manual',
});
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"POST",
"https://api.voicecheap.ai/oauth/token",
headers={"Content-Type":"application/x-www-form-urlencoded"},
data={"grant_type": "authorization_code", "client_id": "mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "code": "mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "redirect_uri": "https://assistant.example.com/oauth/callback", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "resource": "https://api.voicecheap.ai/mcp"},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
Obtain short-lived MCP access tokens using a public client’s S256 authorization code or its current refresh token.
POST https://api.voicecheap.ai/oauth/token
Authentication
No HTTP client authentication. Send client_id in the URL-encoded body. Any Authorization header, client_secret, or client_assertion is rejected.Request body
Show grant_type: authorization_code
Show grant_type: authorization_code
string
required
The client_id returned by dynamic registration, or the exact public HTTPS URL of the client metadata document.Minimum length: 1. Maximum length: 2048. Pattern:
^[^\u0000-\u001F\u007F]+$.string
required
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.Format: “uri”.
field
required
Value defined by the complete input schema.Required value: “authorization_code”.
string
required
Single-use code returned to the exact registered callback.Minimum length: 1. Maximum length: 256. Pattern:
^mcp_code_[A-Za-z0-9_-]{43}$.string
required
Must exactly equal the callback bound to this code and registration.Minimum length: 1. Maximum length: 2048. Pattern:
^[^\u0000-\u001F\u007F]+$.string
required
Original PKCE verifier used to calculate the authorization request’s S256 code_challenge.Minimum length: 43. Maximum length: 128. Pattern:
^[A-Za-z0-9._~-]{43,128}$.Show grant_type: refresh_token
Show grant_type: refresh_token
string
required
The client_id returned by dynamic registration, or the exact public HTTPS URL of the client metadata document.Minimum length: 1. Maximum length: 2048. Pattern:
^[^\u0000-\u001F\u007F]+$.string
required
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.Format: “uri”.
field
required
Value defined by the complete input schema.Required value: “refresh_token”.
string
required
The current unconsumed refresh token from a client registered for refresh_token.Minimum length: 1. Maximum length: 256. Pattern:
^mcp_refresh_[A-Za-z0-9_-]{43}$.string
Space-delimited subset of the original permissions, optionally including offline_access. Unsupported names, empty values, and added permissions are rejected; duplicates are removed. An explicit scope keeps only the listed product and identity permissions and may remove every product permission. Omit scope to retain the current grant, except identity permissions are dropped if OpenID Connect is unavailable.Minimum length: 1. Maximum length: 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))* *$.Technical schema
Technical schema
{
"oneOf": [
{
"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."
},
"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"
]
},
"grant_type": {
"const": "authorization_code"
},
"code": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^mcp_code_[A-Za-z0-9_-]{43}$",
"description": "Single-use code returned to the exact registered callback."
},
"redirect_uri": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"pattern": "^[^\\u0000-\\u001F\\u007F]+$",
"description": "Must exactly equal the callback bound to this code and registration."
},
"code_verifier": {
"type": "string",
"minLength": 43,
"maxLength": 128,
"pattern": "^[A-Za-z0-9._~-]{43,128}$",
"description": "Original PKCE verifier used to calculate the authorization request's S256 code_challenge."
}
},
"required": [
"grant_type",
"client_id",
"resource",
"code",
"redirect_uri",
"code_verifier"
],
"not": {
"anyOf": [
{
"required": [
"client_secret"
]
},
{
"required": [
"client_assertion"
]
}
]
},
"additionalProperties": true
},
{
"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."
},
"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"
]
},
"grant_type": {
"const": "refresh_token"
},
"refresh_token": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^mcp_refresh_[A-Za-z0-9_-]{43}$",
"description": "The current unconsumed refresh token from a client registered for refresh_token."
},
"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 subset of the original permissions, optionally including offline_access. Unsupported names, empty values, and added permissions are rejected; duplicates are removed. An explicit scope keeps only the listed product and identity permissions and may remove every product permission. Omit scope to retain the current grant, except identity permissions are dropped if OpenID Connect is unavailable.",
"examples": [
"account:read projects:read offline_access"
]
}
},
"required": [
"grant_type",
"client_id",
"resource",
"refresh_token"
],
"not": {
"anyOf": [
{
"required": [
"client_secret"
]
},
{
"required": [
"client_assertion"
]
}
]
},
"additionalProperties": true
}
]
}
Request examples
Follow the documented encoding and authentication for this route. The action API’s envelope and idempotency rules apply only to action invocations.curl --request POST 'https://api.voicecheap.ai/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \
--data-urlencode 'code=mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' \
--data-urlencode 'redirect_uri=https://assistant.example.com/oauth/callback' \
--data-urlencode 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk' \
--data-urlencode 'resource=https://api.voicecheap.ai/mcp'
const input = {
grant_type: 'authorization_code',
client_id: 'mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
code: 'mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
redirect_uri: 'https://assistant.example.com/oauth/callback',
code_verifier: 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
resource: 'https://api.voicecheap.ai/mcp',
};
const response = await fetch('https://api.voicecheap.ai/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(input),
redirect: 'manual',
});
console.log(response.status, await response.text());
import os
import requests
response = requests.request(
"POST",
"https://api.voicecheap.ai/oauth/token",
headers={"Content-Type":"application/x-www-form-urlencoded"},
data={"grant_type": "authorization_code", "client_id": "mcp_client_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "code": "mcp_code_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "redirect_uri": "https://assistant.example.com/oauth/callback", "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk", "resource": "https://api.voicecheap.ai/mcp"},
timeout=120,
allow_redirects=False,
)
print(response.status_code, response.text)
Response
An opaque Bearer access token, its remaining lifetime in seconds, granted scopes, and a refresh token when the client registered refresh_token. expires_in is at most 900 and may be smaller near grant expiry.HTTP 200
{
"access_token": "mcp_access_DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "mcp_refresh_EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE",
"scope": "account:read projects:read offline_access"
}
Response headers
{
"Cache-Control": "no-store",
"Pragma": "no-cache"
}
Operation notes
- Recognized scalar parameters must be supplied once as nonempty strings without ASCII control characters or DEL. Repeated query/form fields decode to arrays and are rejected when read as scalars.
- Use URL-encoded form data, such as curl —data-urlencode per field. A JSON token body is rejected with invalid_request.
- The resource audience is required for both grant types and must exactly match the MCP URL of the environment that issued the code or refresh token.
- Authorization-code exchange requires the original client_id, exact redirect_uri, and matching S256 code_verifier. Codes expire after 2 minutes and are single-use.
- The token endpoint rejects every supplied Authorization header, including Basic and Bearer, plus any client_secret or client_assertion body field.
- Access tokens last at most 15 minutes. With refresh_token registered, the connection and refresh family expire 30 days after the user approved it; rotation does not extend that deadline.
- Whether refresh_token is returned depends on the registered grant types. It does not depend on including offline_access in the authorization request. Returned scope appends offline_access whenever a refresh token is returned.
- To renew, send grant_type=refresh_token with client_id, the current refresh_token, resource, and optionally a narrowed scope. Atomically replace the stored refresh token with the new value.
- Reusing a consumed authorization code or rotated refresh token revokes the whole connection, including all of its access tokens. Do not concurrently redeem the same code or refresh token.
- Refresh requests never add permissions. With an explicit scope, only the requested product and identity permissions are retained; offline_access does not preserve omitted product permissions.
- Tokens are bound to the granting user, client, resource audience, current team ownership, and active paid access. A revoked grant or changed ownership requires reconnecting.
- The public token limit is 60 requests per minute per client IP, shared across workers. URL-encoded bodies are limited to 32 KiB.
- OAuth failures return error and error_description. Body-parser failures can be returned earlier in the integration parser’s error form.
- openid and email are identity permissions, separate from product permissions, and are accepted only when advertised. email requires openid. During initial authorization, a request containing only identity scopes uses the default product permissions shown on the consent screen.
- The client_id returned by dynamic registration, or the exact public HTTPS URL of the client metadata document.
- When openid is granted, authorization-code exchange also returns an RS256-signed id_token. Validate its signature, issuer, audience, expiry, and nonce when supplied. Granted identity scopes appear in the token response scope; refresh responses do not issue a new id_token.
- If OpenID Connect becomes unavailable, refreshing without scope keeps the product permissions and removes identity permissions. Explicitly requesting unavailable identity scopes returns invalid_scope.
- If the account was deleted or disabled, the token endpoint returns invalid_grant with HTTP 400.
Errors
| HTTP status | Code | Meaning or recovery |
|---|---|---|
| 400 | invalid_request | The body or a recognized parameter is invalid, missing, duplicated, or has the wrong encoding. |
| 400 | invalid_client | The client_id is unknown, client HTTP authentication was supplied, or a client_secret/client_assertion was supplied. |
| 400 | invalid_target | The resource audience is missing or does not exactly match the MCP resource. |
| 400 | unsupported_grant_type | The grant is unsupported, or refresh_token was not registered for this client. |
| 400 | invalid_grant | The code or token is invalid, expired, revoked, replayed, mismatched, or no longer authorized for the current account/team. |
| 400 | invalid_scope | A refresh request attempts to add permissions or contains unsupported scopes. |
| 413 | REQUEST_TOO_LARGE | The request exceeds the allowed body size. JSON is limited to 1 MiB and URL-encoded forms to 32 KiB. |
| 415 | UNSUPPORTED_CONTENT_TYPE | The integration body parser accepts JSON or URL-encoded forms only. The route may require one of those encodings specifically. |
| 429 | temporarily_unavailable | The public OAuth request rate limit was reached. Wait for the current window before retrying. |
| 500 | server_error | Authorization is temporarily unavailable. Retry later. |
Was this page helpful?

