Skip to content

OAuth 2.0

Third-party applications use OAuth 2.0 authorization code flow to act on behalf of hzel users. All endpoints are served from https://api.hzel.org.


Register a new OAuth application. The client_secret is returned once — store it immediately.

HeaderValueRequired
AuthorizationBearer <token>Yes
Content-Typeapplication/jsonYes

Request Body

FieldTypeRequiredDescription
namestringYesApplication display name
descriptionstringNoShown on the consent screen
redirect_urisstring[]YesMust be https:// in production

Success 200

{
"data": {
"id": "01942cf7-…",
"client_id": "01942cf8-…",
"client_secret": "hzcs_…",
"name": "My App",
"redirect_uris": ["https://myapp.example.com/callback"],
"created_at": "2026-03-28T00:00:00Z"
}
}

Client secrets are prefixed hzcs_ and stored as SHA-256. Use POST /api/v1/oauth/apps/{id}/secret to rotate.

Errors

Statuserror.codeReason
400BAD_REQUESTMissing or invalid fields
401UNAUTHORIZEDMissing or invalid auth

Query Parameters

ParameterTypeRequiredDescription
client_idstringYesYour app’s UUID
redirect_uristringYesMust match a registered redirect URI
response_typestringYesMust be code
statestringYesRandom value; verify on callback to prevent CSRF

Response: 302 Found → hzel consent page.

Errors

Statuserror.codeReason
400BAD_REQUESTMissing or invalid parameters
404NOT_FOUNDclient_id not found

The user sees your app name and grants or denies access via the hzel consent UI.

On approval, the browser is redirected to your redirect_uri:

https://myapp.example.com/callback?code=<auth_code>&state=<your_state>

On denial:

https://myapp.example.com/callback?error=access_denied&state=<your_state>

Always verify the state value matches what you sent in step 1.

HeaderValueRequired
Content-Typeapplication/jsonYes

Request Body

FieldTypeRequiredDescription
grant_typestringYes"authorization_code"
codestringYesThe auth code from the callback
redirect_uristringYesMust exactly match step 1
client_idstringYesYour app’s UUID
client_secretstringYesYour hzcs_… secret

Success 200

{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "hzrt_…"
}

The token endpoint does not wrap the response in a data envelope — it follows the OAuth 2.0 RFC directly.

Errors

Statuserror.codeReason
400BAD_REQUESTInvalid grant_type, bad code, or redirect_uri mismatch
401UNAUTHORIZEDInvalid client_id or client_secret

Use the same POST /api/v1/oauth/token endpoint with grant_type: "refresh_token":

FieldTypeRequiredDescription
grant_typestringYes"refresh_token"
refresh_tokenstringYesThe hzrt_… token from a previous response
client_idstringYesYour app’s UUID
client_secretstringYesYour hzcs_… secret

Refresh tokens rotate on every use — the previous token is revoked immediately.


Revoke a refresh token. Idempotent per RFC 7009.

HeaderValueRequired
Content-Typeapplication/jsonYes

Request Body

FieldTypeRequiredDescription
refresh_tokenstringYesThe hzrt_… token to revoke
client_idstringYesYour app’s UUID
client_secretstringYesYour hzcs_… secret

Success 200{ "data": "revoked" }


All management endpoints require Authorization: Bearer <token>.

MethodEndpointCSRFDescription
GET/api/v1/oauth/appsNoList your apps
GET/api/v1/oauth/apps/{id}NoGet one app
PATCH/api/v1/oauth/apps/{id}Session onlyUpdate name, description, redirect URIs
DELETE/api/v1/oauth/apps/{id}Session onlyRevoke the app and all its tokens
POST/api/v1/oauth/apps/{id}/secretSession onlyRotate the client secret