OAuth 2.1 API
VDClip runs an OAuth 2.1 Authorization Server (issuer https://api.vdclip.com) for MCP servers, AI agents, and third-party integrations that act on behalf of a user. Instead of a static API key, these clients get a short-lived access token through the authorization-code flow with PKCE and call the API with Authorization: Bearer <JWT>.
When to use OAuth
- OAuth 2.1 Bearer token. When a client acts on behalf of a user: an MCP server, AI agent, or third-party integration. The user signs in and consents, the client receives a JWT, and every request carries
Authorization: Bearer <JWT>. - Static
Authorization Bearer. For your own server-to-server calls that do not represent a user. See Authentication.
How the flow works
- Discover server metadata at
/.well-known/oauth-authorization-serverand signing keys at/.well-known/jwks.json. - Register a client with Dynamic Client Registration (
POST /oauth2/application), declaring the scopes it needs. - Authorize: send the user to the dashboard authorization URL from discovery with a PKCE challenge. The dashboard owns consent UI and callback handling.
- Exchange the authorization code for tokens at
POST /oauth2/token. - Call
/v1/*withAuthorization: Bearer <JWT>, and refresh or revoke tokens as needed.
Complete public-client example
Example below uses a public PKCE client. Do not add a client secret. Keep
CLIENT_ID, REGISTRATION_ACCESS_TOKEN, ACCESS_TOKEN, and REFRESH_TOKEN in a
secret store or process memory.
1. Discover endpoints and supported scopes
ISSUER="https://api.vdclip.com"
curl -sS "$ISSUER/.well-known/oauth-authorization-server" | jq .Use authorization_endpoint, token_endpoint, registration_endpoint,
revocation_endpoint, and scopes_supported from response. Do not hard-code
environment-specific hosts when discovery is available.
2. Register client
REGISTRATION=$(curl -sS -X POST "$ISSUER/oauth2/application" \
-H "Content-Type: application/json" \
-d '{
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"redirect_uris": ["https://app.example.com/oauth/callback"],
"client_name": "Example App",
"scope": "vdclip:projects:read vdclip:results:read"
}')
CLIENT_ID=$(jq -r '.client_id' <<<"$REGISTRATION")
REGISTRATION_ACCESS_TOKEN=$(jq -r '.registration_access_token' <<<"$REGISTRATION")Registration response contains registration_access_token once. Store it. It is
different from user access tokens and is required to read registration later.
3. Create PKCE values and open authorization URL
REDIRECT_URI="https://app.example.com/oauth/callback"
STATE=$(openssl rand -hex 16)
CODE_VERIFIER=$(openssl rand -base64 64 | tr -dc 'A-Za-z0-9-._~' | cut -c1-64)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| openssl base64 -A \
| tr '+/' '-_' | tr -d '=')
AUTHORIZATION_ENDPOINT=$(curl -sS "$ISSUER/.well-known/oauth-authorization-server" \
| jq -r '.authorization_endpoint')
echo "$AUTHORIZATION_ENDPOINT?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=vdclip%3Aprojects%3Aread%20vdclip%3Aresults%3Aread&state=$STATE&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"Open printed URL in browser. Verify returned state equals original STATE.
Callback receives code. Never accept callback without matching state.
4. Exchange code for tokens
Public clients send client_id in form body. HTTP Basic is also accepted, but
must identify same client. Send original redirect URI and verifier.
TOKEN_RESPONSE=$(curl -sS -X POST "$ISSUER/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "code=$AUTHORIZATION_CODE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "code_verifier=$CODE_VERIFIER")
ACCESS_TOKEN=$(jq -r '.access_token' <<<"$TOKEN_RESPONSE")
REFRESH_TOKEN=$(jq -r '.refresh_token' <<<"$TOKEN_RESPONSE")5. Call API, refresh, and revoke
curl -sS "$ISSUER/v1/projects?limit=20" \
-H "Authorization: Bearer $ACCESS_TOKEN"
curl -sS -X POST "$ISSUER/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=refresh_token" \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "refresh_token=$REFRESH_TOKEN"
curl -sS -X POST "$ISSUER/oauth2/revoke" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "token=$REFRESH_TOKEN" \
--data-urlencode "token_type_hint=refresh_token" \
--data-urlencode "client_id=$CLIENT_ID"Token and revoke endpoints use RFC-shaped responses. They do not use /v1
errors[] response contract.
Optional and conditional fields
Verified against current API schemas and handlers:
| Request | Required | Optional or conditional behavior |
|---|---|---|
DCR POST /oauth2/application | client_name, redirect_uris keys | token_endpoint_auth_method defaults to none; grant_types defaults to authorization_code; response_types defaults to code; metadata URLs, scope, and allowed_resources can be omitted. Empty redirect_uris works only for refresh-token-only registration. |
| Authorization-code token request | grant_type, client_id or HTTP Basic, code, redirect_uri | code_verifier is conditional on PKCE. Public clients should always send it. scope and resource are not used for this grant. |
| Refresh-token request | grant_type, client_id or HTTP Basic, refresh_token | scope and resource are optional. code, redirect_uri, and code_verifier are not used. |
| Revoke request | token | token_type_hint and client_id are optional. Response is always HTTP 200 with an empty JSON object. |
Optional in schema does not mean valid in every flow. Handler validation applies after form parsing.
Conventions
These endpoints live at /oauth2/* and /.well-known/* and use standard RFC-shaped bodies, not the /v1 envelope. Handle their responses and errors separately. Discovery and JWKS are public; authorize, consent, and token drive the interactive login.
Once a client holds a Bearer token, it calls /v1/* exactly as documented elsewhere. Scopes follow the vdclip:<resource>:<action> convention, with the same plan gating: render:execute and social:publish depend on the user’s plan. The full scope catalog is in MCP Servers. See pricing for plan details.
Standards and RFCs
VDClip implements OAuth 2.1. Each endpoint follows the relevant standard:
| RFC | Specification | Used by |
|---|---|---|
| RFC 6749 | OAuth 2.0 Authorization Framework | Authorization and token endpoints |
| RFC 6750 | Bearer Token Usage | Authorization: Bearer <JWT> on /v1 calls |
| RFC 7636 | Proof Key for Code Exchange (PKCE) | Authorization endpoint |
| RFC 8414 | Authorization Server Metadata | Discovery (/.well-known/oauth-authorization-server) |
| RFC 7517 | JSON Web Key Set (JWKS) | /.well-known/jwks.json |
| RFC 7591 | Dynamic Client Registration | POST /oauth2/application |
| RFC 7592 | Client Registration Management | GET /oauth2/application/{client_id} |
| RFC 9068 | JWT Profile for Access Tokens | Access tokens (at+jwt, ES256) |
| RFC 7009 | Token Revocation | POST /oauth2/revoke |
Endpoints
Server metadata
GET /.well-known/oauth-authorization-server
Register a client
POST /oauth2/application
Read client registration
GET /oauth2/application/{client_id}