This page is generated from openapi.yaml (also served as JSON), so the documentation and the contract are the same file. Every example points at https://visitkorat.com, the environment you are reading.
Discovery
Machine-readable metadata. Fetch these first; hardcode nothing else.
/.well-known/openid-configurationOpenID Provider Metadata
Every endpoint, algorithm and capability this provider supports. Point your client library here rather than hardcoding URLs — it is how an endpoint can move without breaking your integration.
Example request
curl -s https://visitkorat.com/.well-known/openid-configurationResponses
200The provider metadata document.{
"issuer": "https://dev.visitkorat.com",
"authorization_endpoint": "https://dev.visitkorat.com/oauth/authorize",
"token_endpoint": "https://dev.visitkorat.com/oauth/token",
"userinfo_endpoint": "https://dev.visitkorat.com/oauth/userinfo",
"revocation_endpoint": "https://dev.visitkorat.com/oauth/revoke",
"end_session_endpoint": "https://dev.visitkorat.com/oauth/logout",
"jwks_uri": "https://dev.visitkorat.com/.well-known/jwks.json",
"scopes_supported": [
"openid",
"profile",
"email",
"offline_access"
],
"response_types_supported": [
"code"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"id_token_signing_alg_values_supported": [
"ES256"
],
"code_challenge_methods_supported": [
"S256"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
]
}/.well-known/jwks.jsonPublic keys for ID token verification
The public half of every key that may have signed a live ID token —
not only the current one. Match on the kid in the token header.
Cache this, but re-fetch when you meet an unknown kid: that is what a
key rotation looks like from your side, and re-fetching is the whole of
your part in it.
Example request
curl -s https://visitkorat.com/.well-known/jwks.jsonResponses
200A JSON Web Key Set.{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
"kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg",
"alg": "ES256",
"use": "sig"
}
]
}/oauth/openapi.jsonThis document, as JSON
Example request
curl -s https://visitkorat.com/oauth/openapi.jsonResponses
200The OpenAPI 3.1 document./oauth/openapi.yamlThis document, as YAML
Example request
curl -s https://visitkorat.com/oauth/openapi.yamlResponses
200The OpenAPI 3.1 document.Authorization
The browser half of the flow — where the person signs in and consents.
/oauth/logoutEnd the Visit Korat session
A browser redirect. Ends the person's session with Visit Korat and
returns them to a registered post_logout_redirect_uri.
⚠️ This does not sign them out of your app, or of any other
relying party. There is no front- or back-channel logout, and the
discovery document says so. Call /oauth/revoke and clear your own
session as well — and do not tell the person they have been signed out
everywhere, because they have not.
An unregistered post_logout_redirect_uri is ignored, not echoed: this
is a page people reach while signed in, which makes it the best possible
place for an open redirect.
Parameters
| Name | In | Description |
|---|---|---|
id_token_hint | query | An ID token you were issued. Tells us which app is asking. |
client_id | query | Alternative to id_token_hint. |
post_logout_redirect_uri | query | Must be registered on the app, matched exactly. |
state | query |
Example request
curl -s "https://visitkorat.com/oauth/logout"Responses
302To your registered URI, or to a Visit Korat page if none matched.Token
The server half — exchanging a code, refreshing, and revoking.
/oauth/tokenExchange a code, or refresh
From your server. A confidential client that calls this from a browser is refused outright, because a secret that reaches this endpoint from a browser is a secret that shipped in a browser bundle.
Public clients (SPA, native) have no secret and may call it from the browser — but only from an origin registered on the app, which is echoed back one at a time. There is no wildcard CORS here.
Body is application/x-www-form-urlencoded. JSON is refused.
Access tokens are opaque, not JWTs: do not try to decode one. Verify the
id_token instead, or call /oauth/userinfo.
Request body
application/x-www-form-urlencoded
AuthorizationCodeGrant
| Field | Type | Description |
|---|---|---|
grant_typerequired | authorization_code | |
coderequired | string | |
redirect_urirequired | string | The same value you sent to /oauth/authorize. Compared exactly. |
code_verifierrequired | string | |
client_idrequired | string | |
client_secret | string | Confidential clients only. Omit entirely for a public client. |
RefreshTokenGrant
| Field | Type | Description |
|---|---|---|
grant_typerequired | refresh_token | |
refresh_tokenrequired | string | |
client_idrequired | string | |
client_secret | string |
Example request
curl -s -X POST https://visitkorat.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=8f14e45fceea167a5a36dedd4bea2543..." \
-d "redirect_uri=https://example.com/auth/callback" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
-d "client_id=my-app-3f2a91c4" \
-d "client_secret=vkoa_dev_..."curl -s -X POST https://visitkorat.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=vkrt_dev_..." \
-d "client_id=my-app-3f2a91c4" \
-d "client_secret=vkoa_dev_..."curl -s -X POST https://visitkorat.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=8f14e45fceea167a5a36dedd4bea2543..." \
-d "redirect_uri=https://app.example.com/callback" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" \
-d "client_id=my-spa-91c4a3f2"Responses
200Tokens.{
"access_token": "vkat_dev_9f2b1c...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email",
"id_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik5qVkJ..."
}400The request or the grant was bad. A single invalid_grant covers
unknown, expired and already-used codes on purpose — telling them
apart is an oracle for anyone holding a stolen code.
{
"error": "invalid_grant",
"error_description": "Code is unknown, expired or already used."
}{
"error": "invalid_grant",
"error_description": "code_verifier does not match the code_challenge."
}{
"error": "invalid_grant",
"error_description": "This refresh token was already used. Every token from that authorization has been revoked; the user must sign in again."
}{
"error": "invalid_request",
"error_description": "Send application/x-www-form-urlencoded, as RFC 6749 §4.1.3 requires. JSON is not accepted."
}401We could not tell who you are.{
"error": "invalid_client",
"error_description": "Client authentication failed."
}{
"error": "invalid_client",
"error_description": "A confidential client must call this endpoint from its server, never from a browser. Register a public client if you need a browser-based flow."
}429Rate limited — 60 requests per minute per client and address./oauth/revokeRevoke a token (RFC 7009)
Call this when someone signs out of your app. Revoking a refresh token takes its access tokens with it — otherwise "log me out" leaves an hour of working credential behind.
Answers 200 whether or not the token existed. A distinguishable "no
such token" would be an oracle for testing guessed tokens.
Request body
application/x-www-form-urlencoded
| Field | Type | Description |
|---|---|---|
tokenrequired | string | |
token_type_hint | access_token | refresh_token | Optional and ignored — both tables are checked. |
client_id | string | |
client_secret | string |
Example request
curl -s -X POST https://visitkorat.com/oauth/revoke \
-d "token=vkrt_dev_..." \
-d "client_id=my-app-3f2a91c4" \
-d "client_secret=vkoa_dev_..."Responses
200Done — or the token was never ours. You cannot tell, by design.400The request was malformed.401Client authentication failed.Identity
Reading the profile a token stands for.
/oauth/userinfoThe profile behind an access token
Claims follow the scopes the token carries, which may be narrower than
what your app is registered for. sub is always present.
Consent is rechecked on every call. If the person disconnects your app
at /account/connections, this starts answering 401 in the same
second — not when the token expires.
Example request
curl -s https://visitkorat.com/oauth/userinfoResponses
200The claims this token is allowed to see.{
"sub": "6f3f0a5e-3f7a-4a6b-9c1e-2b2f9e4a1d77",
"email": "[email protected]",
"email_verified": true,
"name": "สมชาย ใจดี"
}{
"sub": "6f3f0a5e-3f7a-4a6b-9c1e-2b2f9e4a1d77"
}401No token, or the token is expired, revoked or disowned.WWW-Authenticate: Bearer realm="visitkorat", error="invalid_token", error_description="The access token is expired, revoked or unknown."
{
"error": "invalid_token",
"error_description": "The access token is expired, revoked or unknown."
}{
"error": "invalid_token",
"error_description": "The user has withdrawn consent for this application."
}429Rate limited — 120 requests per minute per token./oauth/userinfoThe profile behind an access token (POST form)
Identical to GET. OIDC Core §5.3.1 allows POST for clients that dislike long headers.
Example request
curl -s -X POST https://visitkorat.com/oauth/userinfoResponses
200As for GET /oauth/userinfo.401As for GET /oauth/userinfo.