API Authentication
The UptimeHunt API uses JWT (JSON Web Token) based authentication to secure all API endpoints.
API Authentication
Overview
The UptimeHunt API uses JWT (JSON Web Token) based authentication to secure all API endpoints. This guide provides comprehensive documentation for implementing authentication in your applications, including token management, user registration, login workflows, and password reset functionality.
Authentication Flow
Token Types
UptimeHunt issues two types of tokens:
| Token Type | Lifetime | Purpose |
|---|---|---|
| Access Token | 15 minutes | Authenticates API requests |
| Refresh Token | 7 days | Generates new access tokens |
Token Management Workflow
- Initial Authentication: User registers or logs in to receive both access and refresh tokens
- API Requests: Include access token in the
Authorizationheader for authenticated requests - Token Refresh: When access token expires, use refresh token to obtain new access token
- Re-authentication: When refresh token expires, user must log in again
Base URL
All API endpoints use the following base URL:
https://app.uptimehunt.io/apiAuthentication Endpoints
User Registration
Create a new user account. Registration is followed by email verification — the account stays inactive until the emailed link is confirmed, and tokens are obtained by logging in afterwards.
Endpoint: POST /v1/auth/register
Authentication: Not required
Request Headers:
Content-Type: application/jsonRequest Body:
{
"email": "user@example.com",
"password": "securepassword123",
"first_name": "John",
"last_name": "Doe"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Valid email address (at most one password account per email) | |
| password | string | Yes | Password (minimum 8 characters) |
| first_name | string | No | User's first name |
| last_name | string | No | User's last name |
| account_type | string | No | personal (default) or team — selects the sign-up path (see below) |
| org_name | string | Team only | Display name of the new team organization; required when account_type is team. The organization's URL slug is derived server-side |
| invite_token | string | No | Invitation sign-up: the token from an organization invitation email (see below) |
Personal vs. team sign-up
account_type: "personal" (the default — existing API consumers are unaffected) creates an individual account with its own personal workspace. account_type: "team" creates the account and only the team organization named by org_name — the creator becomes its Owner and the organization starts a 14-day trial. The founder is an organization-only account with no personal workspace (no personal resource or token side-channel outside the organization's governance); see Organization Context for how such accounts resolve their active organization. A team organization can also be created after a personal sign-up with POST /v1/orgs — in that case you keep your personal workspace. See Creating a Team Organization.
Invitation sign-up
When invite_token carries a valid pending organization invitation, registration completes in one step: the account is created, the email is marked verified (no verification email is sent — the invitation was delivered to that address), and the membership is created with the invited role, all in the same transaction. The response is the distinct REGISTERED body shown below — not the generic VERIFICATION_REQUIRED response — and the user can log in immediately. The submitted email must equal the invitation email — a sign-up with a different address is rejected, as is an invalid, expired, or already-used token (exact status codes and messages in the error table below). Accounts created this way are organization-only (no personal workspace), like team founders; account_type and org_name do not apply on this path.
Success Response (201 Created — personal and team sign-up):
{
"status": "VERIFICATION_REQUIRED",
"message": "If the address is new, an account was created. Check your email for a verification link to activate it, then sign in.",
"email": "user@example.com"
}Anti-enumeration
On the personal and team paths, the endpoint returns this same 201 response whether or not the email was already registered — it never reveals that an account exists. If the address already has an unverified account, the verification email is re-sent (cooldown-limited); a verified account receives nothing.
The invitation path is deliberately not anti-enumerating: the caller already holds the invitation token for that address, so its precise errors (below) reveal nothing they don't already know.
Success Response (201 Created — invitation sign-up):
The invitation path returns a different body: status is REGISTERED, because the account is already verified and active — there is no verification email to check. Send the user straight to login.
{
"status": "REGISTERED",
"message": "Account created and invitation accepted — you can sign in now.",
"email": "invitee@acme.example",
"org": {
"id": 7,
"slug": "acme",
"name": "Acme Corporation",
"org_type": "team",
"logo_url": "",
"url": "",
"contact_email": "",
"enforced_sso": false,
"plan": null,
"billing_state": "trial",
"trial_ends_at": "2026-06-24T10:30:00Z",
"created_at": "2026-05-20T09:00:00Z",
"role": "member",
"capabilities": ["member.view", "org.view", "service.view", "..."]
}
}org is the organization the account just joined, in the same shape GET /v1/orgs returns it — including the new member's role and effective capabilities.
Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | Email and password are required | Missing required fields |
| 400 | account_type must be one of: personal, team | Unknown account_type value |
| 400 | org_name is required for team signup | account_type is team but no organization name was provided |
Invitation sign-up (invite_token present) adds these:
| Status Code | Error | Description |
|---|---|---|
| 400 | Invalid invitation token | invite_token does not match any invitation |
| 400 | The email address must match the invitation — sign up with the address the invitation was sent to | Submitted email differs from the invitation's email |
| 400 | invite_token cannot be combined with a team signup — the invitation already determines the organization | Both invite_token and account_type: "team" were sent |
| 403 | Organization member limit reached (N) | The invitation's organization is at its plan's member limit (N is the limit) |
| 409 | This invitation was already used | The invitation was already accepted |
| 409 | An account with this email already exists — sign in with it and accept the invitation instead | A password account with this email exists — log in with it and use the authenticated accept endpoint |
| 410 | This invitation has expired | The invitation passed its expiry — ask an organization Owner or Admin to send a new one |
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"first_name": "John",
"last_name": "Doe"
}'cURL Example (team sign-up):
curl -X POST https://app.uptimehunt.io/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "founder@acme.example",
"password": "securepassword123",
"account_type": "team",
"org_name": "Acme Corporation"
}'cURL Example (invitation sign-up):
curl -X POST https://app.uptimehunt.io/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "invitee@acme.example",
"password": "securepassword123",
"invite_token": "token_from_the_invitation_email"
}'User Login
Authenticate existing user and receive authentication tokens.
Endpoint: POST /v1/auth/login
Authentication: Not required
Request Headers:
Content-Type: application/jsonRequest Body:
{
"email": "user@example.com",
"password": "securepassword123"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's email address | |
| password | string | Yes | User's password |
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"user": {
"id": 1,
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"date_joined": "2024-01-15T10:30:00Z"
}
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | Must include "email" and "password" | Missing required fields |
| 400 | Invalid credentials | Incorrect email or password |
| 400 | User account is disabled | Account has been deactivated |
| 403 | SSO_REQUIRED | The account belongs to an organization with enforced SSO — password login is refused; authenticate through the organization's identity provider instead |
Email-first login and SSO
The same email address may have multiple sign-in routes (a password account and/or organization SSO accounts — they are separate accounts). Use the SSO lookup endpoint to discover the routes before prompting for a password.
The SSO_REQUIRED Payload
Every endpoint that refuses password authentication under enforced SSO — login, token refresh, password reset and password change — returns the same structured 403 body:
{
"code": "SSO_REQUIRED",
"message": "Your organization requires single sign-on. Sign in through your identity provider.",
"org": "acme",
"org_slug": "acme",
"redirect_url": "https://auth.uptimehunt.io/accounts/oidc/acme/login/?process=login",
"idps": [
{
"org_slug": "acme",
"label": "Acme Corporation",
"redirect_url": "https://auth.uptimehunt.io/accounts/oidc/acme/login/?process=login",
"enforced": true
}
]
}| Field | Description |
|---|---|
org_slug | The enforcing organization's slug — always present. When redirect_url is null, fall back to the org_slug form of the lookup endpoint, or at least name the organization in a "contact your administrator" message |
org | The same value as org_slug, kept for existing consumers |
redirect_url | The organization's IdP entry URL — navigate the browser there to sign in. null when the organization enforces SSO but its identity provider is missing or disabled; password authentication is refused regardless |
idps | The enforcing organization's sign-in route in the exact lookup route shape (org_slug, label, redirect_url, enforced). The payload is self-sufficient: render the "Continue with single sign-on" action from this response alone — no separate lookup call and no previously stored lookup result needed. Contains exactly one route when the organization's identity provider is enabled; [] exactly when redirect_url is null (so an empty idps is the structured "no route available — contact your administrator" signal; a route object never appears with a null URL) |
idps[].label | The organization's display name — use it to label the SSO button |
idps[].enforced | Always true here — this payload only fires for an organization that enforces SSO |
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'Refresh Access Token
Generate a new access token using a valid refresh token.
Endpoint: POST /v1/auth/refresh
Authentication: Not required (uses refresh token)
Request Headers:
Content-Type: application/jsonRequest Body:
{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | Yes | Valid refresh token |
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | Refresh token is required | Missing refresh token |
| 401 | Invalid refresh token | Token is invalid, expired, malformed, or has been revoked |
| 403 | SSO_REQUIRED | The account belongs to an enforced-SSO organization and this refresh token predates enforcement — re-authenticate via SSO |
Refresh-token rotation and revocation
Refresh tokens are rotated: each refresh invalidates the used token and the response includes a new refresh_token — always store the returned pair. Outstanding refresh tokens are also revoked server-side on organization events such as member removal or role downgrade and when enforced SSO is enabled.
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}'Get Current User
Retrieve authenticated user information.
Endpoint: GET /v1/auth/me
Authentication: Required
Request Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Success Response (200 OK):
{
"data": {
"id": 1,
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"date_joined": "2024-01-15T10:30:00Z",
"realm": {"type": "password"},
"default_org_slug": "u-1",
"orgs": [
{
"id": 7,
"slug": "acme",
"name": "Acme Corporation",
"org_type": "team",
"role": "admin",
"capabilities": ["org.view", "member.view", "member.invite", "audit.view", "service.view", "service.create", "service.edit", "service.delete", "..."]
}
]
}
}| Field | Description |
|---|---|
realm | The account's identity realm: {"type": "password"}, {"type": "social", "provider": "github"} for social sign-in accounts, or {"type": "sso", "org_slug": "...", "idp_label": "..."} for SSO-provisioned accounts |
orgs | All organizations the account belongs to, with its role and effective capabilities in each |
default_org_slug | The organization used when a request carries no organization context |
Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 401 | Authentication credentials were not provided | Missing Authorization header |
| 401 | Given token not valid for any token type | Invalid or expired access token |
cURL Example:
curl -X GET https://app.uptimehunt.io/api/v1/auth/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Update User Profile
Update authenticated user's profile information.
Endpoint: PUT /v1/auth/profile or PATCH /v1/auth/profile
Authentication: Required
Request Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/jsonRequest Body (all fields optional for PATCH):
{
"email": "newemail@example.com",
"first_name": "Jane",
"last_name": "Smith"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| string | No | New email address (must be unique) | |
| first_name | string | No | Updated first name |
| last_name | string | No | Updated last name |
Success Response (200 OK):
{
"data": {
"id": 1,
"email": "newemail@example.com",
"first_name": "Jane",
"last_name": "Smith",
"date_joined": "2024-01-15T10:30:00Z"
},
"message": "Profile updated successfully"
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | A user with this email already exists | Email already in use |
| 401 | Authentication credentials were not provided | Missing or invalid token |
cURL Example:
curl -X PUT https://app.uptimehunt.io/api/v1/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"first_name": "Jane",
"last_name": "Smith"
}'Change Password
Change authenticated user's password.
Endpoint: POST /v1/auth/change-password
Authentication: Required
Request Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/jsonRequest Body:
{
"current_password": "oldpassword123",
"new_password": "newsecurepassword456"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| current_password | string | Yes | Current password for verification |
| new_password | string | Yes | New password (minimum 8 characters) |
Success Response (200 OK):
{
"status": "PASSWORD_CHANGED",
"message": "Password changed successfully"
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | Current password is incorrect | Wrong current password provided |
| 400 | This field is required | Missing required field |
| 400 | Ensure this field has at least 8 characters | Password too short |
| 401 | Authentication credentials were not provided | Missing or invalid token |
| 403 | SSO_REQUIRED | The account belongs to an organization with enforced SSO — local passwords are disabled by policy, so they cannot be changed (the same gate as login and password reset) |
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/change-password \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"current_password": "oldpassword123",
"new_password": "newsecurepassword456"
}'Security Notice
A confirmation email is sent to the user after successfully changing their password.
Request Password Reset
Request a password reset link via email.
Endpoint: POST /v1/auth/password-reset/request
Authentication: Not required
Request Headers:
Content-Type: application/jsonRequest Body:
{
"email": "user@example.com",
"frontend_url": "https://app.uptimehunt.io"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Email address of the account | |
| frontend_url | string | No | Base URL for password reset link (defaults to configured frontend URL) |
Success Response (200 OK):
{
"status": "PASSWORD_RESET_SENT",
"detail": "If an account with that email exists, a password reset link has been sent."
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | Email is required | Missing email field |
| 500 | Failed to send password reset email | Email service unavailable |
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/password-reset/request \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"frontend_url": "https://app.uptimehunt.io"
}'Security Design
The response is intentionally vague to prevent email enumeration attacks. The same response is returned whether the email exists or not.
Confirm Password Reset
Complete the password reset process using the token from email.
Endpoint: POST /v1/auth/password-reset/confirm
Authentication: Not required
Request Headers:
Content-Type: application/jsonRequest Body:
{
"uid": "MQ",
"token": "c5h7ke-8f9a2b3c4d5e6f7g8h9i0j1k2",
"new_password": "newsecurepassword456"
}Request Schema:
| Field | Type | Required | Description |
|---|---|---|---|
| uid | string | Yes | Base64-encoded user ID from reset email |
| token | string | Yes | Password reset token from email |
| new_password | string | Yes | New password (minimum 8 characters) |
Success Response (200 OK):
{
"status": "PASSWORD_RESET_CONFIRMED",
"detail": "Password has been reset successfully"
}Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 400 | UID, token, and new password are required | Missing required fields |
| 400 | Invalid reset link | Invalid or malformed UID/token |
| 400 | Invalid or expired reset link | Token has expired or already used |
| 400 | Password must be at least 8 characters long | Password too short |
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/password-reset/confirm \
-H "Content-Type: application/json" \
-d '{
"uid": "MQ",
"token": "c5h7ke-8f9a2b3c4d5e6f7g8h9i0j1k2",
"new_password": "newsecurepassword456"
}'Confirmation Email
A confirmation email is sent to the user after successfully resetting their password.
Enforced SSO
Password-reset requests for accounts that belong to an organization with enforced SSO are refused with a 403 SSO_REQUIRED payload — those accounts have no usable password by policy.
Single Sign-On (SSO)
Organizations can connect their own identity provider (OIDC, SAML 2.0, Google Workspace, GitHub) — see the SSO guides for setup and semantics. These endpoints support SSO and social login flows in custom clients.
SSO Route Lookup
Discover every sign-in route available for an email address. Because email is a display attribute and not an identity key, one email may map to multiple separate accounts — at most one password account, plus one per social provider, plus one per organization identity provider (identity realms). Login UIs should be email-first: call this endpoint, then render the matching password prompt, social buttons, and/or an account chooser.
Endpoint: POST /v1/auth/sso/lookup
Authentication: Not required (rate-limited)
Request Body — send exactly one of the two keys:
{
"email": "alex@example.com"
}or, for direct organization entry (the login page's Use single sign-on option):
{
"org_slug": "acme"
}Success Response (200 OK):
{
"password": true,
"idps": [
{
"org_slug": "acme",
"label": "Acme Corporation",
"redirect_url": "https://auth.uptimehunt.io/accounts/saml/acme/login/?process=login",
"enforced": false
},
{
"org_slug": "umbrella-corp",
"label": "Umbrella Corp (Google Workspace)",
"redirect_url": "https://auth.uptimehunt.io/accounts/oidc/umbrella-corp/login/?process=login",
"enforced": true
}
],
"social": []
}All three keys are always present.
| Field | Description |
|---|---|
password | Whether password sign-in is available for this email — show the password field. false when no password account exists or the account's organization enforces SSO (its password route is withheld from the chooser) |
idps | SSO routes for this email: organizations whose identity provider covers the email's verified domain, where an SSO account with this email already exists, or where the email's password account is a member of an organization with enforced SSO (membership grants the route even without a verified domain). Empty when none |
idps[].enforced | true when that organization enforces SSO for this account. Clients should auto-redirect only when exactly one route exists, it is enforced, and password is false — every other combination renders explicit choices, never a surprise redirect |
social | Platform social sign-in routes ({provider, label, redirect_url} each). Populated only when the email's domain is a known public mail provider (gmail.com, outlook.com, …) and the platform has social providers configured; company-looking domains always get []. Derived from the domain alone — never from account lookups — so identical for known and unknown addresses on the same domain |
The response shape is constant — the same structure is returned whether or not any routes exist, and the endpoint is rate-limited to make account enumeration impractical. The configured social providers can also be discovered without an email via GET /v1/auth/social-providers — the same provider set under that endpoint's own field names (its id ≡ provider here, login_url ≡ redirect_url).
Lookup by organization slug: the org_slug form returns that organization's SSO route (password is always false on this path, and so is idps[].enforced — without an email there is no account to evaluate enforcement against; social is always []). It works for any enabled identity provider regardless of domain verification — this is how members reach IdPs that are never email-routed for their first sign-in, and what powers the organization sign-in link …/login?org={slug}. An unknown slug, an organization without an identity provider, and one whose provider is disabled all return the identical {"password": false, "idps": [], "social": []}, so the endpoint does not reveal whether an organization exists. When both keys are sent, email takes precedence.
cURL Example:
curl -X POST https://app.uptimehunt.io/api/v1/auth/sso/lookup \
-H "Content-Type: application/json" \
-d '{"email": "alex@example.com"}'
curl -X POST https://app.uptimehunt.io/api/v1/auth/sso/lookup \
-H "Content-Type: application/json" \
-d '{"org_slug": "acme"}'Browser Flow and Token Exchange
A full SSO sign-in works like this:
- Client calls lookup (above) and the user picks an SSO route.
- Browser navigates to the route's
redirect_url— the UptimeHunt authentication host (auth.uptimehunt.io) redirects to the organization's IdP. - After IdP authentication, the browser lands back on the application at
/auth/sso/callback?code={handle}, wherecodeis a one-time, single-use handle valid for 60 seconds. - The client exchanges the handle for the standard JWT pair:
Endpoint: POST /v1/auth/sso/exchange
Request Body:
{
"code": "{handle}"
}Success Response (200 OK): the same token pair as login (access_token, refresh_token, token_type, expires_in, user).
Error Responses:
| Status Code | Error | Description |
|---|---|---|
| 401 | Invalid or expired sign-in code | The handle is malformed, older than 60 seconds, or has already been used |
From here on, SSO-authenticated clients use the exact same access/refresh token mechanics as password-authenticated ones. Platform social sign-ins ride the exact same handoff: the browser lands on /auth/sso/callback?code={handle} and the client exchanges the handle identically.
Social Sign-In Providers
Discover the platform's configured social sign-in providers — the email-independent companion to the lookup's social[] key. The provider set is the same, but the item field names are this endpoint's own: id carries what the lookup returns as provider, and login_url what it returns as redirect_url (label is shared). The two shapes are both intentional — don't assume the items are interchangeable.
Endpoint: GET /v1/auth/social-providers
Authentication: Not required
Success Response (200 OK):
{
"providers": [
{"id": "google", "label": "Google", "login_url": "https://auth.uptimehunt.io/accounts/google/login/?process=login"},
{"id": "github", "label": "GitHub", "login_url": "https://auth.uptimehunt.io/accounts/github/login/?process=login"}
]
}providers is empty when no social provider is configured on the deployment — clients then render no social buttons at all.
GitHub Organization Access
Accounts signed in with the platform GitHub provider can discover and join organizations mapped to their GitHub organizations — see SSO with GitHub for the full model and error codes:
| Endpoint | Purpose |
|---|---|
GET /v1/auth/github/orgs | Mapped organizations with join state (member / eligible) |
POST /v1/auth/github/orgs/{slug}/join | Explicitly join a mapped organization (verified against GitHub at that moment) |
Service Account Tokens for Automation
Machine clients — Terraform, Ansible, the Kubernetes auto-discovery operator, CI pipelines — should authenticate with a service account token (also called an organization token) rather than a personal API token:
- Org-pinned: the token is bound to exactly one organization at creation. No
X-Org-Slugheader needed; the active org is always the bound org. - Scoped: capabilities are capped at the automation-safe
read-onlyorautomationpresets. The token can never gain SSO, member-management, billing, or token-oversight access. - Durable: the token survives staff changes — it is attached to the organization, not to a person.
Mint a service token at Settings → Tokens → Service tokens (UI) or POST /api/v1/orgs/{slug}/tokens (API). See Service Account / Organization Tokens for the full guide, including preset definitions and consumer-specific examples (Kubernetes, Terraform, Ansible).
Organization Context (X-Org-Slug)
All resource endpoints (/services, /projects, /integrations, /alert-rules, /incidents, …) operate on the request's active organization. The optional X-Org-Slug header selects it:
curl https://app.uptimehunt.io/api/v1/services \
-H "Authorization: Bearer {access_token}" \
-H "X-Org-Slug: acme"Resolution order when determining the active organization:
X-Org-Slugheader — must name an organization the authenticated user is a member of.- The API token's organization binding — service account tokens are bound to an organization at creation; requests authenticated with such a token resolve to that org automatically. Sending
X-Org-Slugto a different org returns403. This lets header-less machine clients (Terraform, Ansible, the Kubernetes operator) target a team organization without any header. - The user's personal workspace, if they have one.
- The user's sole organization, if they belong to exactly one.
- Otherwise the request is rejected with
403 {"code": "ORG_CONTEXT_REQUIRED"}— a multi-organization account must say which organization it means.
Organization-only accounts have no personal workspace
Not every account has a personal workspace: team-founder sign-ups, invitation sign-ups, and SSO-provisioned (JIT) members are organization-only accounts. For them step 3 never applies — an account with a single membership resolves through step 4 (its sole organization), and an organization-only account that belongs to several organizations must send X-Org-Slug (or authenticate with an organization-bound API token), otherwise the request fails with ORG_CONTEXT_REQUIRED. The web app always sends the header, so this only concerns custom API clients.
Properties:
- Backward compatible: a single-workspace user sending no header gets exactly the pre-organizations behavior.
- An
X-Org-Slugnaming an unknown organization or one you are not a member of returns404in both cases — the API does not reveal which organizations exist. - Role capabilities are evaluated against the active organization on every request — see Roles & Permissions.
- Organization-management endpoints live under
/v1/orgs/{slug}/...and take the slug from the path instead.
Structured Permission Errors
Authorization failures return machine-readable bodies so clients and IaC tooling can produce actionable diagnostics.
Capability denied (403):
{
"error": "permission_denied",
"required": "service.edit",
"org": "acme"
}required is the missing capability; org is the active organization the check ran against.
Other structured 403 codes:
| Code | Meaning |
|---|---|
SSO_REQUIRED | Password authentication refused — the account's organization enforces SSO; follow redirect_url, or fall back to the org_slug field when it is null (full payload) |
TRIAL_EXPIRED | Write rejected — the active organization's trial has expired; reads still work |
ORG_CONTEXT_REQUIRED | No organization context could be resolved — send X-Org-Slug |
Using Authentication Tokens
Token Storage
Store tokens securely in your application:
- Web Applications: Use secure HTTP-only cookies or browser localStorage
- Mobile Applications: Use secure storage mechanisms (Keychain, KeyStore)
- Server-to-Server: Use environment variables or secure credential storage
Making Authenticated Requests
Include the access token in the Authorization header for all authenticated API requests:
Authorization: Bearer {access_token}Example Request:
curl -X GET https://app.uptimehunt.io/api/v1/services \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Handling Token Expiration
Access tokens expire after 15 minutes. Implement token refresh logic:
// JavaScript/TypeScript example
async function apiRequest(url, options = {}) {
let accessToken = localStorage.getItem('access_token');
// Add authorization header
options.headers = {
...options.headers,
'Authorization': `Bearer ${accessToken}`
};
let response = await fetch(url, options);
// If token expired, refresh and retry
if (response.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
// Refresh the token
const refreshResponse = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (refreshResponse.ok) {
const { access_token } = await refreshResponse.json();
localStorage.setItem('access_token', access_token);
// Retry original request with new token
options.headers['Authorization'] = `Bearer ${access_token}`;
response = await fetch(url, options);
} else {
// Refresh failed, redirect to login
window.location.href = '/login';
}
}
return response;
}# Python example using requests
import requests
from datetime import datetime, timedelta
class APIClient:
def __init__(self, base_url):
self.base_url = base_url
self.access_token = None
self.refresh_token = None
self.token_expiry = None
def login(self, email, password):
response = requests.post(
f"{self.base_url}/v1/auth/login",
json={"email": email, "password": password}
)
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.refresh_token = data['refresh_token']
# Token expires in 900 seconds (15 minutes)
self.token_expiry = datetime.now() + timedelta(seconds=data['expires_in'])
return data['user']
def refresh_access_token(self):
response = requests.post(
f"{self.base_url}/v1/auth/refresh",
json={"refresh_token": self.refresh_token}
)
response.raise_for_status()
data = response.json()
self.access_token = data['access_token']
self.token_expiry = datetime.now() + timedelta(seconds=data['expires_in'])
def request(self, method, endpoint, **kwargs):
# Refresh token if expired or about to expire
if self.token_expiry and datetime.now() >= self.token_expiry - timedelta(minutes=1):
self.refresh_access_token()
headers = kwargs.pop('headers', {})
headers['Authorization'] = f"Bearer {self.access_token}"
response = requests.request(
method,
f"{self.base_url}{endpoint}",
headers=headers,
**kwargs
)
# Handle token expiration
if response.status_code == 401:
self.refresh_access_token()
headers['Authorization'] = f"Bearer {self.access_token}"
response = requests.request(
method,
f"{self.base_url}{endpoint}",
headers=headers,
**kwargs
)
return response
# Usage
client = APIClient("https://app.uptimehunt.io/api")
client.login("user@example.com", "password123")
response = client.request("GET", "/v1/services")
services = response.json()Security Best Practices
Token Security
- Secure Storage: Never store tokens in plain text or client-side code
- HTTPS Only: Always use HTTPS in production to prevent token interception
- Token Rotation: Implement regular token refresh to minimize security risks
- Logout: Invalidate tokens on logout by removing them from storage
Password Security
- Minimum Length: Enforce minimum 8-character passwords
- Complexity: Use passwords with mixed character types
- Unique Passwords: Never reuse passwords across services
- Password Managers: Recommend using password managers
API Security
- Input Validation: Validate all input data on the client and server
- Error Handling: Don't expose sensitive information in error messages
- Monitoring: Monitor authentication attempts for suspicious activity
Common Authentication Errors
401 Unauthorized
Cause: Missing, invalid, or expired authentication token
Solutions:
- Verify token is included in
Authorizationheader - Check token format:
Bearer {token} - Refresh expired access token using refresh token
- Re-authenticate if refresh token is expired
400 Bad Request
Cause: Invalid request parameters or validation errors
Solutions:
- Verify all required fields are provided
- Check field formats (email, password length)
- Review error response for specific validation messages
403 Forbidden
Cause: Valid authentication but insufficient permissions
Solutions:
- Inspect the structured error body — it names the missing capability or policy code (
SSO_REQUIRED,TRIAL_EXPIRED,ORG_CONTEXT_REQUIRED) - Verify your role in the active organization carries the required capability
- Check the organization context the request resolved to
- Contact an organization Owner for access
JWT Token Structure
UptimeHunt uses JWT tokens with the following configuration:
| Setting | Value | Description |
|---|---|---|
| Algorithm | HS256 | HMAC with SHA-256 |
| Access Token Lifetime | 15 minutes | Validity period for access tokens |
| Refresh Token Lifetime | 7 days | Validity period for refresh tokens |
| Header Type | Bearer | Authorization header type |
| User ID Claim | user_id | Claim containing user identifier |
Token Payload Example
{
"user_id": 1,
"token_type": "access",
"exp": 1705318200,
"iat": 1705317300,
"jti": "abc123def456"
}| Claim | Description |
|---|---|
| user_id | Unique identifier for the authenticated user |
| token_type | Token type (access or refresh) |
| exp | Expiration timestamp (Unix time) |
| iat | Issued at timestamp (Unix time) |
| jti | JWT ID (unique token identifier) |
Related Documentation
- Service Account / Organization Tokens - Org-pinned credentials for Terraform, Ansible, and the Kubernetes operator
- Services API - Managing monitoring services
- Projects API - Organizing services with projects
- Organizations - Team workspaces, roles, SSO, audit log
- Single Sign-On - OIDC, SAML 2.0, Google Workspace setup
- Account Management - User guide for account settings
- Registration Guide - User registration walkthrough
API Reference Overview
The UptimeHunt API provides programmatic access to all monitoring platform features, enabling you to automate service management, retrieve metrics, and integrate monitoring into your existing workflows.
Services API Reference
The Services API enables programmatic management of monitoring services in UptimeHunt.