ScamVerify

OAuth Flow

Authenticate with the ScamVerify™ MCP server and API using OAuth 2.1 with PKCE.

ScamVerify™ implements OAuth 2.1 with PKCE (Proof Key for Code Exchange) for MCP clients and third-party applications. This allows users to authorize applications without sharing API keys.

Most MCP clients (Claude Desktop, Cursor) handle OAuth automatically. You only need this guide if you are building a custom integration or want to understand the flow.

Discovery Endpoints

ScamVerify™ publishes OAuth metadata at standard well-known URLs:

EndpointPurpose
GET /.well-known/oauth-authorization-serverRFC 8414 authorization server metadata
GET /.well-known/oauth-protected-resourceRFC 9728 protected resource metadata

Authorization Server Metadata

curl https://scamverify.ai/.well-known/oauth-authorization-server
{
  "issuer": "https://scamverify.ai",
  "authorization_endpoint": "https://scamverify.ai/api/oauth/authorize",
  "token_endpoint": "https://scamverify.ai/api/oauth/token",
  "registration_endpoint": "https://scamverify.ai/api/oauth/register",
  "scopes_supported": [
    "phone:lookup",
    "url:lookup",
    "text:analyze",
    "email:analyze",
    "usage:read"
  ],
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"]
}

Protected Resource Metadata

curl https://scamverify.ai/.well-known/oauth-protected-resource
{
  "resource": "https://scamverify.ai/api/mcp",
  "authorization_servers": ["https://scamverify.ai"],
  "scopes_supported": [
    "phone:lookup",
    "url:lookup",
    "text:analyze",
    "email:analyze",
    "usage:read"
  ]
}

OAuth Flow Step by Step

Register your client

Register a new OAuth client with the registration endpoint. You need at least one redirect URI from the allowlist.

curl -X POST https://scamverify.ai/api/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My MCP App",
    "redirect_uris": ["http://localhost:3000/callback"]
  }'

Response:

{
  "client_id": "client_abc123...",
  "client_secret": "secret_xyz789...",
  "client_name": "My MCP App",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}

Store the client_secret securely. It is only returned once during registration.

Generate PKCE challenge

Generate a code verifier (random string) and its S256 challenge:

import crypto from 'crypto';

const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto
  .createHash('sha256')
  .update(codeVerifier)
  .digest('base64url');
import secrets
import hashlib
import base64

code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()

Redirect user to authorize

Build the authorization URL and redirect the user:

https://scamverify.ai/api/oauth/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=http://localhost:3000/callback&
  code_challenge=CODE_CHALLENGE&
  code_challenge_method=S256&
  state=RANDOM_STATE&
  scope=phone:lookup url:lookup text:analyze email:analyze usage:read

The user will see a ScamVerify™-branded consent screen showing the requested permissions. If the user is not signed in, they will be redirected to the login page first.

Exchange code for tokens

After the user approves, they are redirected to your redirect_uri with an authorization code:

http://localhost:3000/callback?code=AUTH_CODE&state=RANDOM_STATE

Exchange the code for tokens:

curl -X POST https://scamverify.ai/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "redirect_uri": "http://localhost:3000/callback",
    "client_id": "CLIENT_ID",
    "client_secret": "CLIENT_SECRET",
    "code_verifier": "CODE_VERIFIER"
  }'
const response = await fetch("https://scamverify.ai/api/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code: authCode,
    redirect_uri: "http://localhost:3000/callback",
    client_id: clientId,
    client_secret: clientSecret,
    code_verifier: codeVerifier,
  }),
});

const tokens = await response.json();
import requests

response = requests.post(
    "https://scamverify.ai/api/oauth/token",
    json={
        "grant_type": "authorization_code",
        "code": auth_code,
        "redirect_uri": "http://localhost:3000/callback",
        "client_id": client_id,
        "client_secret": client_secret,
        "code_verifier": code_verifier,
    },
)

tokens = response.json()

Response:

{
  "access_token": "svat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "svrt_...",
  "scope": "phone:lookup url:lookup text:analyze email:analyze usage:read"
}

Use the access token

Include the access token in MCP requests or direct API calls:

curl -X POST https://scamverify.ai/api/mcp \
  -H "Authorization: Bearer svat_YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/list",
    "id": 1
  }'

Refresh expired tokens

Access tokens expire after 1 hour. Use the refresh token to get a new pair:

curl -X POST https://scamverify.ai/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "svrt_YOUR_REFRESH_TOKEN",
    "client_id": "CLIENT_ID",
    "client_secret": "CLIENT_SECRET"
  }'

Refresh tokens are rotated on each use. The old refresh token is invalidated and a new one is returned. Refresh tokens expire after 30 days.

Scopes

Request only the scopes your application needs:

ScopePermission
phone:lookupLook up phone number risk assessments
url:lookupCheck website and URL safety
text:analyzeAnalyze text messages for scams
email:analyzeAnalyze emails for phishing
usage:readView API usage and quota

If no scope is specified, all scopes are granted.

Allowed Redirect URIs

For security, redirect URIs must match one of these patterns:

PatternUse Case
https://*.claude.ai/*Claude Desktop / claude.ai
https://*.anthropic.com/*Anthropic services
https://*.chatgpt.com/*ChatGPT
https://*.openai.com/*OpenAI platform
https://*.smithery.ai/*Smithery MCP registry
https://*.run.tools/*run.tools MCP platform
http://localhost:*/*Local development (any port)
http://127.0.0.1:*/*Local development (any port)

Exact matches for common OAuth redirect URLs are also supported (e.g., https://chatgpt.com/connector_platform_oauth_redirect).

Token Details

PropertyAccess TokenRefresh Token
Prefixsvat_svrt_
TTL1 hour30 days
StorageSHA-256 hashed (never stored in plaintext)SHA-256 hashed
RotationNew token on each refreshRotated on use

Error Responses

Invalid Client

{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}

Invalid Grant

Returned when the authorization code is expired, already used, or the PKCE verifier does not match:

{
  "error": "invalid_grant",
  "error_description": "Authorization code is invalid or expired"
}

Unsupported Grant Type

{
  "error": "unsupported_grant_type",
  "error_description": "Only authorization_code and refresh_token grants are supported"
}

Security Notes

  • PKCE is mandatory. All authorization requests must include code_challenge with method S256. Plain challenge method is not supported.
  • Authorization codes are one-time use. A code can only be exchanged for tokens once. Subsequent attempts will fail.
  • Authorization codes expire in 5 minutes. Exchange them promptly after the user approves.
  • Tokens are stored as SHA-256 hashes. ScamVerify™ never stores tokens in plaintext.
  • CSRF protection. The authorization endpoint uses cookie-based nonce validation.

On this page