Authentication
Most Haltless API endpoints authenticate with a bearer JWT access token. Telemetry ingestion authenticates with an API key instead. See Sensor Data and the API Keys guide.
Send the access token on every authenticated request:
Authorization: Bearer <jwt>
Access tokens last 30 minutes. A refresh token, valid for 7 days, is returned as a secure, httpOnly cookie by /api/v1/auth/login and is used by /api/v1/auth/refresh to mint a new access token. Each refresh replaces the cookie, and the old one stops working.
Endpoints
| Method | Path | Description |
|---|---|---|
POST | /api/v1/auth/login | Exchange email and password for an access token |
POST | /api/v1/auth/refresh | Get a fresh access token from the refresh cookie |
GET | /api/v1/auth/me | Return the authenticated user's profile |
POST | /api/v1/auth/logout | Revoke the current session |
POST | /api/v1/auth/mfa/verify | Complete an MFA challenge for a full token |
POST | /api/v1/auth/forgot-password | Email a password-reset link |
POST | /api/v1/auth/reset-password | Set a new password from a reset token |
GET | /api/v1/auth/sso/check | Check whether an email domain uses SSO |
POST | /api/v1/auth/sso/exchange | Exchange an SSO code for tokens |
Log in
POST /api/v1/auth/login
Request body
{
"email": "user@example.com",
"password": "<password>",
"tenant_id": null
}
tenant_id is optional. Send it only when a previous response asked you to choose an organization (see below).
Response: 200 OK
{
"access_token": "<jwt>",
"token_type": "bearer",
"mfa_required": false,
"tenant_choice_required": false,
"tenants": [],
"password_expired": false
}
| Field | Description |
|---|---|
mfa_required | true when the account has multi-factor authentication enabled. access_token is then a pending token, valid for 5 minutes and only for completing the challenge |
tenant_choice_required | true when the email and password match accounts in more than one organization. access_token is then empty and tenants lists the organizations (tenant_id, name); repeat the request with the chosen tenant_id |
password_expired | true when the password is over 90 days old. Until it is changed, requests other than changing it return 403 |
Errors
| Status | Meaning |
|---|---|
401 | Wrong email or password |
403 | The email address has not been verified yet |
423 | The company's account is suspended |
429 | Too many failed attempts: 10 failures within 10 minutes lock the account for 15 minutes |
Refresh an access token
POST /api/v1/auth/refresh
Sends the httpOnly refresh cookie set at login; no request body is required.
Response: 200 OK
{
"access_token": "<jwt>",
"token_type": "bearer"
}
Complete an MFA challenge
POST /api/v1/auth/mfa/verify
Request body
{
"mfa_token": "<pending-token>",
"code": "123456"
}
The code accepts a six-digit time-based one-time code or a single-use recovery code.
Response: 200 OK
{
"access_token": "<jwt>",
"token_type": "bearer"
}
Get the current user
GET /api/v1/auth/me
Requires a bearer JWT. Returns the authenticated user's profile.
Response: 200 OK
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"email": "user@example.com",
"full_name": "Jordan Rivera",
"role": "operator",
"mfa_enabled": true,
"timezone": "UTC",
"locale": "en",
"last_login_at": "2026-08-07T09:12:00Z"
}
Log out
POST /api/v1/auth/logout
Requires a bearer JWT. Revokes the current access and refresh tokens and clears the refresh cookie. Returns 204 No Content.
Password reset
POST /api/v1/auth/forgot-password emails a time-limited reset link to the address if it matches a user. It always returns 204 No Content, so it never reveals whether an email is registered.
{
"email": "user@example.com"
}
POST /api/v1/auth/reset-password consumes the emailed token and sets a new password:
{
"token": "<reset-token>",
"new_password": "<new-password>"
}
Returns 204 No Content on success.
Single sign-on
If your organization uses SSO, check a login email's domain before showing a password field:
GET /api/v1/auth/sso/check?email=user@example.com
Response: 200 OK
{
"has_sso": true,
"provider_name": "Example IdP",
"redirect_url": "https://idp.example.com/authorize?..."
}
Redirect the user to redirect_url. After the identity provider authenticates the user, your login page receives a short-lived code that you exchange for tokens:
POST /api/v1/auth/sso/exchange
{
"code": "<sso-code>"
}
Response: 200 OK
{
"access_token": "<jwt>",
"token_type": "bearer"
}
See the Authentication getting-started guide for a full walkthrough.