Skip to content
Developers

API reference

Every endpoint, with real requests and responses — generated from openapi.yaml.

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.

get/.well-known/openid-configuration

OpenID 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

bash
curl -s https://visitkorat.com/.well-known/openid-configuration

Responses

200The provider metadata document.
json
{
  "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"
  ]
}
get/.well-known/jwks.json

Public 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

bash
curl -s https://visitkorat.com/.well-known/jwks.json

Responses

200A JSON Web Key Set.
json
{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
      "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
      "kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDAyQjVDNjk1RTM2Qg",
      "alg": "ES256",
      "use": "sig"
    }
  ]
}
get/oauth/openapi.json

This document, as JSON

Example request

bash
curl -s https://visitkorat.com/oauth/openapi.json

Responses

200The OpenAPI 3.1 document.
get/oauth/openapi.yaml

This document, as YAML

Example request

bash
curl -s https://visitkorat.com/oauth/openapi.yaml

Responses

200The OpenAPI 3.1 document.

Authorization

The browser half of the flow — where the person signs in and consents.

get/oauth/authorize

Send the person here to sign in and consent

A browser redirect, not an API call — never fetch this with XHR.

The person signs in with a Visit Korat magic link, sees exactly what your app is asking for, and agrees or declines. On success they come back to your redirect_uri with code and state.

PKCE is mandatory. Generate a random code_verifier (43–128 characters from A-Za-z0-9-._~), send its base64url SHA-256 as code_challenge, and keep the verifier for the token call. plain is refused.

Errors do not always come back to you. If client_id or redirect_uri cannot be verified, the person sees an error page and is redirected nowhere — forwarding a browser to an unverified URI is how an authorize endpoint becomes an open redirect. Everything else is reported to your redirect_uri as ?error=.

Parameters

NameInDescription
client_idrequiredquery
e.g. my-app-3f2a91c4
redirect_urirequiredqueryMust match one of your registered URIs exactly. No wildcards.
e.g. https://example.com/auth/callback
response_typerequiredquery
code
scoperequiredquerySpace-delimited. Must include openid.
e.g. openid email profile
staterequiredqueryYour CSRF token, returned untouched. Required, not merely recommended: we cannot verify it for you, and an integration without it is one login-CSRF away from attaching a victim's session to an attacker's account.
code_challengerequiredquerybase64url SHA-256 of your code_verifier — 43 characters.
e.g. E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
code_challenge_methodrequiredquery
S256
noncequeryReturned in the ID token. Send one and check it to bind the token to this request.
terms_versionrequiredqueryThe version of your terms the person is agreeing to. Stored with the consent record, and a change forces a fresh prompt — which is what makes the record mean something under PDPA.
e.g. 2026-08-01
privacy_versionrequiredqueryThe version of your privacy policy. Same rules.
e.g. 2026-08-01

Example request

bash
curl -s "https://visitkorat.com/oauth/authorize?client_id=my-app-3f2a91c4&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fcallback&response_type=%3Cresponse_type%3E&scope=openid%20email%20profile&state=%3Cstate%3E&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=%3Ccode_challenge_method%3E&terms_version=2026-08-01&privacy_version=2026-08-01"

Responses

200An HTML page — the sign-in form, the consent screen, or an error that must not be redirected.
302Back to your redirect_uri, with either code + state or error + error_description + state.
get/oauth/logout

End 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

NameInDescription
id_token_hintqueryAn ID token you were issued. Tells us which app is asking.
client_idqueryAlternative to id_token_hint.
post_logout_redirect_uriqueryMust be registered on the app, matched exactly.
statequery

Example request

bash
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.

post/oauth/token

Exchange 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

FieldTypeDescription
grant_typerequiredauthorization_code
coderequiredstring
redirect_urirequiredstringThe same value you sent to /oauth/authorize. Compared exactly.
code_verifierrequiredstring
client_idrequiredstring
client_secretstringConfidential clients only. Omit entirely for a public client.

RefreshTokenGrant

FieldTypeDescription
grant_typerequiredrefresh_token
refresh_tokenrequiredstring
client_idrequiredstring
client_secretstring

Example request

bash
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_..."
bashTrade a refresh token for a new pair
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_..."
bashA public client — no secret, PKCE only
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.
json
{
  "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.
json
{
  "error": "invalid_grant",
  "error_description": "Code is unknown, expired or already used."
}
json
{
  "error": "invalid_grant",
  "error_description": "code_verifier does not match the code_challenge."
}
json
{
  "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."
}
json
{
  "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.
json
{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}
json
{
  "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.
post/oauth/revoke

Revoke 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

FieldTypeDescription
tokenrequiredstring
token_type_hintaccess_token | refresh_tokenOptional and ignored — both tables are checked.
client_idstring
client_secretstring

Example request

bash
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.

get/oauth/userinfo

The 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

bash
curl -s https://visitkorat.com/oauth/userinfo

Responses

200The claims this token is allowed to see.
jsonopenid email profile
{
  "sub": "6f3f0a5e-3f7a-4a6b-9c1e-2b2f9e4a1d77",
  "email": "[email protected]",
  "email_verified": true,
  "name": "สมชาย ใจดี"
}
jsonopenid only
{
  "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."

json
{
  "error": "invalid_token",
  "error_description": "The access token is expired, revoked or unknown."
}
json
{
  "error": "invalid_token",
  "error_description": "The user has withdrawn consent for this application."
}
429Rate limited — 120 requests per minute per token.
post/oauth/userinfo

The 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

bash
curl -s -X POST https://visitkorat.com/oauth/userinfo

Responses

200As for GET /oauth/userinfo.
401As for GET /oauth/userinfo.