The HRMLESS API uses OAuth 2.0 (OpenID Connect) for authentication via Keycloak.
Using Python? Our Python SDK handles authentication automatically - you don't need to implement this manually!
Authentication Flow
HRMLESS uses the OAuth 2.0 Password Grant flow:
- Initial Login: Exchange username/password for access and refresh tokens
- Use Access Token: Include in API requests (valid for ~30 minutes)
- Refresh Token: Get new access token before expiration (refresh valid for ~30 minutes)
- Repeat: Continue refreshing or re-authenticate when refresh token expires
Obtaining Tokens
Endpoint
CodePOST https://login.hrmless.com/realms/nervai/protocol/openid-connect/token
Request
Bash
Codecurl -X POST https://login.hrmless.com/realms/nervai/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "client_id=nervai_frontend" \ -d "username=YOUR_USERNAME" \ -d "password=YOUR_PASSWORD"
Python
Codeimport requests response = requests.post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", data={ "grant_type": "password", "client_id": "nervai_frontend", "username": "YOUR_USERNAME", "password": "YOUR_PASSWORD" } ) tokens = response.json() access_token = tokens["access_token"] refresh_token = tokens["refresh_token"]
Note: The Python SDK does this automatically!
Go
Codepackage main import ( "encoding/json" "io" "net/http" "net/url" "strings" ) type TokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` RefreshExpiresIn int `json:"refresh_expires_in"` TokenType string `json:"token_type"` } func authenticate(username, password string) (*TokenResponse, error) { data := url.Values{} data.Set("grant_type", "password") data.Set("client_id", "nervai_frontend") data.Set("username", username) data.Set("password", password) resp, err := http.Post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), ) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var tokens TokenResponse if err := json.Unmarshal(body, &tokens); err != nil { return nil, err } return &tokens, nil }
JavaScript
Codeasync function authenticate(username, password) { const response = await fetch( 'https://login.hrmless.com/realms/nervai/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'password', client_id: 'nervai_frontend', username: username, password: password, }), } ); const tokens = await response.json(); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresIn: tokens.expires_in, }; }
Response
Code{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 1800, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "not-before-policy": 0, "session_state": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "scope": "email profile" }
Response Fields
| Field | Description |
|---|---|
access_token | JWT token to use in API requests |
expires_in | Access token lifetime in seconds (~1800 = 30 minutes) |
refresh_token | Token to get a new access token |
refresh_expires_in | Refresh token lifetime in seconds (~1800 = 30 minutes) |
token_type | Always "Bearer" |
Using the Access Token
Include the access token in the Authorization header of all API requests:
cURL
Codecurl -X GET https://api-nervai.hrmless.com/api/v1/org_id \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Python
Codeimport requests headers = { "Authorization": f"Bearer {access_token}" } response = requests.get( "https://api-nervai.hrmless.com/api/v1/org_id", headers=headers ) data = response.json()
Go
Codereq, err := http.NewRequest( "GET", "https://api-nervai.hrmless.com/api/v1/org_id", nil, ) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+accessToken) client := &http.Client{} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close()
JavaScript
Codeconst response = await fetch( 'https://api-nervai.hrmless.com/api/v1/org_id', { headers: { 'Authorization': `Bearer ${accessToken}`, }, } ); const data = await response.json();
Refreshing the Access Token
When your access token expires (after ~30 minutes), use the refresh token to get a new one:
cURL
Codecurl -X POST https://login.hrmless.com/realms/nervai/protocol/openid-connect/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=nervai_frontend" \ -d "refresh_token=YOUR_REFRESH_TOKEN"
Python
Codeimport requests response = requests.post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", data={ "grant_type": "refresh_token", "client_id": "nervai_frontend", "refresh_token": refresh_token } ) new_tokens = response.json() access_token = new_tokens["access_token"] refresh_token = new_tokens["refresh_token"] # Also gets a new refresh token
Go
Codefunc refreshToken(refreshToken string) (*TokenResponse, error) { data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("client_id", "nervai_frontend") data.Set("refresh_token", refreshToken) resp, err := http.Post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), ) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var tokens TokenResponse if err := json.Unmarshal(body, &tokens); err != nil { return nil, err } return &tokens, nil }
JavaScript
Codeasync function refreshAccessToken(refreshToken) { const response = await fetch( 'https://login.hrmless.com/realms/nervai/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: 'nervai_frontend', refresh_token: refreshToken, }), } ); const tokens = await response.json(); return { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresIn: tokens.expires_in, }; }
Important: The refresh token is also refreshed with each request. Always use the new refresh token from the response.
Complete Example with Token Management
Here's a complete example showing authentication and automatic token refresh:
Python
Codeimport requests import json from datetime import datetime, timedelta class HRMLessAuth: def __init__(self, username, password): self.username = username self.password = password self.access_token = None self.refresh_token = None self.token_expires_at = None self.authenticate() def authenticate(self): """Get initial tokens""" response = requests.post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", data={ "grant_type": "password", "client_id": "nervai_frontend", "username": self.username, "password": self.password } ) response.raise_for_status() tokens = response.json() self.access_token = tokens["access_token"] self.refresh_token = tokens["refresh_token"] # Set expiration (refresh 2 minutes before actual expiry) expires_in = tokens["expires_in"] self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 120) def refresh(self): """Refresh the access token""" response = requests.post( "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", data={ "grant_type": "refresh_token", "client_id": "nervai_frontend", "refresh_token": self.refresh_token } ) response.raise_for_status() tokens = response.json() self.access_token = tokens["access_token"] self.refresh_token = tokens["refresh_token"] expires_in = tokens["expires_in"] self.token_expires_at = datetime.now() + timedelta(seconds=expires_in - 120) def get_token(self): """Get current valid token, refreshing if needed""" if datetime.now() >= self.token_expires_at: self.refresh() return self.access_token def request(self, method, endpoint, **kwargs): """Make an authenticated API request""" token = self.get_token() headers = kwargs.get('headers', {}) headers['Authorization'] = f'Bearer {token}' kwargs['headers'] = headers url = f"https://api-nervai.hrmless.com/api/v1/{endpoint.lstrip('/')}" response = requests.request(method, url, **kwargs) # Handle 401 - token might have expired, retry once if response.status_code == 401: self.refresh() headers['Authorization'] = f'Bearer {self.access_token}' response = requests.request(method, url, **kwargs) response.raise_for_status() return response.json() # Usage auth = HRMLessAuth("your_username", "your_password") # Get organization ID org_data = auth.request("GET", "/org_id") org_id = org_data["org_id"] # Get positions (token auto-refreshes if needed) positions = auth.request("GET", f"/org/{org_id}/position") print(f"Found {len(positions['items'])} positions")
Note: This is simplified - the Python SDK does all this automatically with background token refresh!
Go
Codepackage main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type HRMLessClient struct { Username string Password string AccessToken string RefreshToken string TokenExpiresAt time.Time BaseURL string AuthURL string } type TokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` RefreshExpiresIn int `json:"refresh_expires_in"` TokenType string `json:"token_type"` } func NewHRMLessClient(username, password string) (*HRMLessClient, error) { client := &HRMLessClient{ Username: username, Password: password, BaseURL: "https://api-nervai.hrmless.com/api/v1", AuthURL: "https://login.hrmless.com/realms/nervai/protocol/openid-connect/token", } if err := client.Authenticate(); err != nil { return nil, err } return client, nil } func (c *HRMLessClient) Authenticate() error { data := url.Values{} data.Set("grant_type", "password") data.Set("client_id", "nervai_frontend") data.Set("username", c.Username) data.Set("password", c.Password) resp, err := http.Post( c.AuthURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), ) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return err } var tokens TokenResponse if err := json.Unmarshal(body, &tokens); err != nil { return err } c.AccessToken = tokens.AccessToken c.RefreshToken = tokens.RefreshToken // Set expiration (refresh 2 minutes before actual expiry) c.TokenExpiresAt = time.Now().Add(time.Duration(tokens.ExpiresIn-120) * time.Second) return nil } func (c *HRMLessClient) Refresh() error { data := url.Values{} data.Set("grant_type", "refresh_token") data.Set("client_id", "nervai_frontend") data.Set("refresh_token", c.RefreshToken) resp, err := http.Post( c.AuthURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()), ) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return err } var tokens TokenResponse if err := json.Unmarshal(body, &tokens); err != nil { return err } c.AccessToken = tokens.AccessToken c.RefreshToken = tokens.RefreshToken c.TokenExpiresAt = time.Now().Add(time.Duration(tokens.ExpiresIn-120) * time.Second) return nil } func (c *HRMLessClient) GetToken() (string, error) { if time.Now().After(c.TokenExpiresAt) { if err := c.Refresh(); err != nil { return "", err } } return c.AccessToken, nil } func (c *HRMLessClient) Request(method, endpoint string, body interface{}) ([]byte, error) { token, err := c.GetToken() if err != nil { return nil, err } var reqBody io.Reader if body != nil { jsonData, err := json.Marshal(body) if err != nil { return nil, err } reqBody = bytes.NewBuffer(jsonData) } url := c.BaseURL + "/" + strings.TrimPrefix(endpoint, "/") req, err := http.NewRequest(method, url, reqBody) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Handle 401 - retry once with refreshed token if resp.StatusCode == 401 { if err := c.Refresh(); err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.AccessToken) resp, err = client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() } return io.ReadAll(resp.Body) } // Usage example func main() { client, err := NewHRMLessClient("your_username", "your_password") if err != nil { panic(err) } // Get organization ID data, err := client.Request("GET", "/org_id", nil) if err != nil { panic(err) } var orgData map[string]interface{} json.Unmarshal(data, &orgData) orgID := orgData["org_id"].(string) fmt.Printf("Organization ID: %s\n", orgID) // Get positions positionsData, err := client.Request("GET", fmt.Sprintf("/org/%s/position", orgID), nil) if err != nil { panic(err) } fmt.Printf("Positions: %s\n", string(positionsData)) }
JavaScript
Codeclass HRMLessClient { constructor(username, password) { this.username = username; this.password = password; this.accessToken = null; this.refreshToken = null; this.tokenExpiresAt = null; this.baseURL = 'https://api-nervai.hrmless.com/api/v1'; this.authURL = 'https://login.hrmless.com/realms/nervai/protocol/openid-connect/token'; } async authenticate() { const response = await fetch(this.authURL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'password', client_id: 'nervai_frontend', username: this.username, password: this.password, }), }); if (!response.ok) { throw new Error('Authentication failed'); } const tokens = await response.json(); this.accessToken = tokens.access_token; this.refreshToken = tokens.refresh_token; // Set expiration (refresh 2 minutes before actual expiry) const expiresIn = tokens.expires_in; this.tokenExpiresAt = new Date(Date.now() + (expiresIn - 120) * 1000); } async refresh() { const response = await fetch(this.authURL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: 'nervai_frontend', refresh_token: this.refreshToken, }), }); if (!response.ok) { throw new Error('Token refresh failed'); } const tokens = await response.json(); this.accessToken = tokens.access_token; this.refreshToken = tokens.refresh_token; const expiresIn = tokens.expires_in; this.tokenExpiresAt = new Date(Date.now() + (expiresIn - 120) * 1000); } async getToken() { if (new Date() >= this.tokenExpiresAt) { await this.refresh(); } return this.accessToken; } async request(method, endpoint, data = null) { const token = await this.getToken(); const options = { method, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }; if (data) { options.body = JSON.stringify(data); } const url = `${this.baseURL}/${endpoint.replace(/^\//, '')}`; let response = await fetch(url, options); // Handle 401 - retry once with refreshed token if (response.status === 401) { await this.refresh(); options.headers['Authorization'] = `Bearer ${this.accessToken}`; response = await fetch(url, options); } if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } return await response.json(); } } // Usage (async () => { const client = new HRMLessClient('your_username', 'your_password'); await client.authenticate(); // Get organization ID const orgData = await client.request('GET', '/org_id'); const orgId = orgData.org_id; console.log(`Organization ID: ${orgId}`); // Get positions const positions = await client.request('GET', `/org/${orgId}/position`); console.log(`Found ${positions.items.length} positions`); })();
Best Practices
1. Refresh Proactively
Refresh your token 1-2 minutes before it expires, not after:
Code# ✅ Good - Refresh before expiration if datetime.now() >= token_expires_at - timedelta(minutes=2): refresh_token() # ❌ Bad - Wait for 401 error # This adds latency to user requests
2. Handle Refresh Token Expiration
If the refresh token expires, you must re-authenticate with username/password:
Codetry: refresh_token() except requests.HTTPError as e: if e.response.status_code == 401: # Refresh token expired, need fresh login authenticate(username, password)
3. Store Tokens Securely
- Never commit tokens to version control
- Use environment variables or secure credential stores
- Don't log tokens in plaintext
- Rotate credentials regularly
4. Implement Retry Logic
Handle transient failures gracefully:
Codeimport time def request_with_retry(method, url, max_retries=3): for attempt in range(max_retries): try: response = requests.request(method, url, headers=headers) response.raise_for_status() return response except requests.HTTPError as e: if e.response.status_code == 401: refresh_token() headers['Authorization'] = f'Bearer {access_token}' elif attempt < max_retries - 1: time.sleep(2 ** attempt) # Exponential backoff else: raise
Common Issues
401 Unauthorized
Cause: Token expired or invalid
Solution: Refresh your token or re-authenticate
Code# Get new token curl -X POST https://login.hrmless.com/realms/nervai/protocol/openid-connect/token \ -d "grant_type=refresh_token" \ -d "client_id=nervai_frontend" \ -d "refresh_token=YOUR_REFRESH_TOKEN"
400 Bad Request (invalid_grant)
Cause: Refresh token expired or invalid credentials
Solution: Re-authenticate with username/password
Token Expires Too Quickly
Cause: Server time mismatch or miscalculated expiration
Solution: Parse the JWT exp claim for accurate expiration:
Codeimport json import base64 from datetime import datetime def parse_jwt_expiry(token): # Split token and decode payload payload = token.split('.')[1] # Add padding if needed payload += '=' * (4 - len(payload) % 4) decoded = json.loads(base64.urlsafe_b64decode(payload)) # Get expiration timestamp exp_timestamp = decoded['exp'] return datetime.fromtimestamp(exp_timestamp)
Security Considerations
- Use HTTPS: All authentication requests must use HTTPS
- Don't share tokens: Each user should have their own credentials
- Rotate credentials: Change passwords periodically
- Monitor usage: Watch for unusual API activity
- Implement rate limiting: Avoid hitting API limits
Want an easier way? Use our Python SDK which handles all of this automatically with background token refresh and retry logic!
Next Steps
- 📚 Python SDK Examples - Automatic authentication
- 🔌 API Reference - Explore all endpoints
- 💡 Best Practices - Production tips