Use an authorization code when you need the user's phone or email, or want to call DiiL later without the user.
const code = await Diil.requestAuthCode() // DiiL shows the consent sheet the first time
await fetch('/login', { method: 'POST', body: JSON.stringify({ code }) })
The code is valid for 60 seconds and works once. Exchange it on your server.
The address of the DiiL API (the base for the endpoints below) is shown in For business → My apps in the DiiL app.
Exchange the code: POST /oauth/token #
Send application/x-www-form-urlencoded (JSON is accepted too). Client credentials go in the body or as HTTP Basic (Authorization: Basic base64(client_id:client_secret)).
grant_type=authorization_code&code=Xk9…&client_id=21&client_secret=diil_sk_…
{ "access_token": "eyJhbGciOiJIUzI1NiJ9…", "token_type": "Bearer", "expires_in": 3600,
"refresh_token": "diil_rt_…", "refresh_expires_in": 2592000, "scope": "profile username email" }
access_tokenlives 1 hour. Use it for the userinfo endpoint. Treat it as opaque.refresh_tokenlives 30 days and is single use. Every refresh returns a new one: store it and drop the old one. Presenting an already used refresh token revokes the whole chain (we assume it leaked), and the user has to sign in again. Presenting a used code does the same to the tokens issued from it.
Refresh #
grant_type=refresh_token&refresh_token=diil_rt_…&client_id=21&client_secret=diil_sk_…
The new tokens never carry more scopes than the user currently grants.
Read the profile: GET /oauth/userinfo #
Authorization: Bearer <access_token>
{ "sub": "4", "id": 4, "firstName": "Anna", "lastName": null, "languageCode": "en",
"username": "anna", "avatarUrl": "https://…",
"phone": "+66900000004", "email": "anna@example.com", "emailVerified": true,
"scope": "profile username avatar phone email" }
Only the granted scopes' fields are present. If the user revokes access, the secret is rotated or the token expires, you get 401 invalid_token.
Sign out: POST /oauth/revoke #
token=<access or refresh token>&client_id=…&client_secret=… returns 200 (always, as in RFC 7009). Either token revokes every refresh token of that consent, so no new access tokens can be issued. A revoked access token also stops working at once; other access tokens already issued expire within the hour. Call it when the user signs out of your app.
Errors #
OAuth-style JSON: {"error": "invalid_grant", "error_description": "The code has expired"}.
| Status | error |
Meaning |
|---|---|---|
| 400 | invalid_request |
A parameter is missing or the body is malformed. |
| 400 | invalid_grant |
Unknown, expired or already used code or refresh token, or the user revoked access. |
| 400 | unsupported_grant_type |
Not authorization_code or refresh_token. |
| 401 | invalid_client |
Unknown client_id or wrong client_secret. |
| 401 | invalid_token |
Userinfo: bad, expired or revoked access token (also WWW-Authenticate). |
| 403 | invalid_origin |
A browser request from an origin not in allowedOrigins. |
| 429 | slow_down |
Rate limit; see Retry-After. |
Rate limits #
/oauth/tokenand/oauth/revoke: 60 requests per minute per client.- 10 failed client authentications per 15 minutes per IP.
/oauth/userinfo: 120 per minute per user and app.requestAuthCode: 30 per minute per user.
CORS #
Browser calls to /oauth/* must come from allowedOrigins. Keep the code exchange on your server anyway: the client secret must never reach the browser.