API Reference
All endpoints are served under /api/v1/. The API uses JSON for both
requests and responses.
Authentication
The API supports two authentication methods:
Session authentication
The web UI authenticates with Django sessions (cookies). CSRF protection is enforced for all mutating requests when using session authentication.
Service token authentication
The CLI and other programmatic clients authenticate with a Bearer token:
Authorization: Bearer hlp_<token>
Service tokens are created through the API (see the /auth/cli-token/ and
/auth/tokens/ endpoints below) and are stored as salted hashes — the raw token
is visible only at creation time.
Roles and permissions
Project memberships have four roles:
Role |
Capabilities |
|---|---|
owner |
Full control: can delete the project, manage members, manage secrets |
admin |
Can manage members, manage secrets, invite users |
member |
Can read and write secrets |
viewer |
Read-only access to secrets |
Read access (GET) is available to any project member. Write access (POST, PUT, PATCH, DELETE) to secrets and environments requires the member role or above. Member management requires admin or owner.
Health
- GET /api/v1/health/
Unauthenticated health check.
Example response:
{"status": "ok"}
Authentication endpoints
- POST /api/v1/auth/login/
Authenticate with email and password. Sets a session cookie.
Request body:
{ "email": "user@example.com", "password": "s3cret123" }
Success (200):
{ "id": "uuid", "email": "user@example.com", "first_name": "", "last_name": "", "is_superuser": false, "has_password": true }
Failure (401):
{"detail": "Invalid email or password."}
- POST /api/v1/auth/logout/
End the current session. Requires authentication.
- GET /api/v1/auth/me/
Return the authenticated user’s profile. Requires authentication.
Response:
{ "id": "uuid", "email": "user@example.com", "first_name": "", "last_name": "", "is_superuser": false, "has_password": true, "sso_enabled": false }
- PATCH /api/v1/auth/me/
Update the authenticated user’s first and/or last name. Requires a browser session (not a service token).
Request body:
{ "first_name": "Jane", "last_name": "Doe" }
- POST /api/v1/auth/password/
Change the authenticated user’s password. Requires a browser session.
Request body:
{ "current_password": "old-password", "new_password": "new-password" }
Failure (400):
{"current_password": "Current password is incorrect."}
Service tokens
- POST /api/v1/auth/cli-token/
Create a service token for CLI authentication. Requires authentication. The token does not expire by default.
- GET /api/v1/auth/tokens/
List the authenticated user’s active (non-revoked) service tokens.
- POST /api/v1/auth/tokens/
Create a service token with optional name and expiry. Requires authentication.
Request body:
{ "name": "CI pipeline", "expires_days": 90 }
expires_daysis optional;1–3650days. Without it, the token never expires.Response (201):
{ "token": "hlp_...", "id": "uuid", "name": "CI pipeline", "prefix": "hlp_abc12345", "created_at": "...", "last_used_at": null, "expires_at": "2026-09-18T00:00:00Z", "revoked": false, "is_expired": false }
Warning
The token value is shown only once at creation time and cannot be
retrieved again.
- DELETE /api/v1/auth/tokens/(uuid: pk)/
Revoke a service token. Requires authentication.
Response: 204 No Content
Projects
- GET /api/v1/projects/
List projects the authenticated user is a member of.
Response:
[ { "id": "uuid", "name": "Web App", "slug": "web-app", "description": "Main web application", "role": "admin", "created_at": "...", "updated_at": "..." } ]
- POST /api/v1/projects/
Create a new project. The authenticated user becomes the owner.
Request body:
{ "name": "My Project", "description": "Optional description" }
Response (201):
{ "id": "uuid", "name": "My Project", "slug": "my-project", "description": "Optional description", "role": "owner", "created_at": "...", "updated_at": "..." }
- GET /api/v1/projects/(uuid: pk)/
Retrieve a project.
- PUT /api/v1/projects/(uuid: pk)/
Update a project. Requires admin or owner role.
Request body: same fields as create (
name,description).
- PATCH /api/v1/projects/(uuid: pk)/
Partially update a project. Requires admin or owner role.
- DELETE /api/v1/projects/(uuid: pk)/
Delete a project. Requires owner role.
Project members
- GET /api/v1/projects/(uuid: project_id)/members/
List members of a project.
Response:
[ { "id": 1, "user_id": "uuid", "email": "user@example.com", "first_name": "Jane", "last_name": "Doe", "role": "admin" } ]
- POST /api/v1/projects/(uuid: project_id)/members/
Add or update a member. Requires admin or owner role.
Request body:
{ "email": "newuser@example.com", "role": "member" }
roleis one of:owner,admin,member,viewer. Defaults tomember.If the target user is already a member, their role is updated.
- DELETE /api/v1/projects/(uuid: project_id)/members/(int: user_id)/
Remove a member from a project. Requires admin or owner role. Cannot remove the last owner.
Project invitations
- GET /api/v1/projects/(uuid: project_id)/invitations/
List pending invitations for a project. Requires admin or owner role.
- POST /api/v1/projects/(uuid: project_id)/invitations/
Create an invitation. Requires admin or owner role.
Request body:
{ "email": "invitee@example.com", "role": "member" }
If the email already has a pending invitation for this project, returns 400.
- DELETE /api/v1/projects/(uuid: project_id)/invitations/(uuid: pk)/
Revoke a pending invitation. Requires admin or owner role.
- POST /api/v1/projects/(uuid: project_id)/invitations/(uuid: pk)/resend/
Rotate the token and extend the expiry of a pending invitation. Requires admin or owner role.
- GET /api/v1/invitations/(str: token)/
Public (unauthenticated) lookup of an invitation by its raw token.
Response:
{ "project_id": "uuid", "project_name": "Web App", "email": "invitee@example.com", "role": "member", "status": "pending", "invited_by_email": "admin@example.com", "created_at": "...", "expires_at": "...", "is_expired": false, "account_exists": true }
- POST /api/v1/invitations/(str: token)/accept/
Accept an invitation. If the invited email matches an existing user who is authenticated, the membership is created directly. If no account exists, a new one is created with the provided password (minimum 8 characters).
Request body (new user):
{ "password": "new-password", "first_name": "Jane", "last_name": "Doe" }
Response (201, new account):
{ "accepted": true, "created_account": true, "project_id": "uuid" }
Environments
- GET /api/v1/projects/(uuid: project_id)/environments/
List environments for a project.
Response:
[ {"id": "uuid", "name": "Development", "slug": "development"}, {"id": "uuid", "name": "Production", "slug": "production"} ]
- POST /api/v1/projects/(uuid: project_id)/environments/
Create an environment. Requires member role or above (write access).
Request body:
{"name": "Staging"}
- GET /api/v1/projects/(uuid: project_id)/environments/(uuid: pk)/
Retrieve an environment.
- PUT /api/v1/projects/(uuid: project_id)/environments/(uuid: pk)/
Update an environment. Requires write access.
- PATCH /api/v1/projects/(uuid: project_id)/environments/(uuid: pk)/
Partially update an environment. Requires write access.
- DELETE /api/v1/projects/(uuid: project_id)/environments/(uuid: pk)/
Delete an environment. Requires write access.
Secrets
- GET /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/
List secrets in an environment. Values are returned in plaintext.
Response:
[ { "id": "uuid", "key": "DATABASE_URL", "value": "postgres://...", "comment": "Primary DB" } ]
- POST /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/
Create a secret. Requires write access.
Request body:
{ "key": "API_KEY", "value": "sk_live_abc123", "comment": "Production API key" }
Returns 400 if a secret with the same key already exists in the environment.
- GET /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/(uuid: pk)/
Retrieve a single secret.
- PUT /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/(uuid: pk)/
Replace a secret. Requires write access.
- PATCH /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/(uuid: pk)/
Partially update a secret. Requires write access.
- DELETE /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/(uuid: pk)/
Delete a secret. Requires write access.
- GET /api/v1/projects/(uuid: project_id)/environments/(uuid: env_id)/secrets/export/
Export all secrets as a flat key/value array. This endpoint is used by the CLI
runcommand.Response:
[ {"key": "DATABASE_URL", "value": "postgres://..."}, {"key": "API_KEY", "value": "sk_live_abc123"} ]
Onboarding / setup
These endpoints are public (AllowAny) and only available while no
administrator account exists. Once an instance is configured, they return 403.
- GET /api/v1/setup/status/
Report the current onboarding state.
Response (onboarding required):
{ "onboarding_required": true, "phase": "config", "config_complete": false, "has_superuser": false, "restart_required": false, "current": { "base_url": "", "database_engine": "django.db.backends.sqlite3", "database_type": "sqlite", "database_url_masked": "", "encryption_key_set": false } }
Response (onboarding complete):
{"onboarding_required": false}
- POST /api/v1/setup/apply-config/
Write base URL, database URL, and encryption key to the config file (phase 1).
Request body:
{ "base_url": "https://heartlock.example.com", "database_url": "postgres://user:pass@db:5432/heartlock", "encryption_key": "" }
If
encryption_keyis empty, one is generated automatically. If the database URL or encryption key changed, the response includes"restart_required": true.
- POST /api/v1/setup/restart/
Trigger a process exit so a container restart policy applies the new configuration. Only available when a restart is genuinely pending.
Response:
{"restarting": true}
- POST /api/v1/setup/create-admin/
Create the first superuser (phase 2). Only available after the configuration phase is complete and no restart is pending.
Request body:
{ "email": "admin@example.com", "password": "secure-password", "first_name": "Admin", "last_name": "User" }
Password must be at least 8 characters.
Superuser administration
All admin endpoints require an authenticated superuser. They are mounted
under /api/v1/admin/.
Overview
- GET /api/v1/admin/overview/
Aggregate instance statistics and read-only configuration.
Response:
{ "stats": { "users": 5, "superusers": 1, "active_users": 4, "projects": 3, "environments": 6, "secrets": 42, "pending_invitations": 1, "accepted_invitations": 8, "service_tokens": 2, "active_service_tokens": 2 }, "config": { "debug": false, "oidc_enabled": true, "email_configured": true, "email_backend": "django.core.mail.backends.smtp.EmailBackend", "invite_expiry_days": 7, "encryption_key_set": true, "database_engine": "django.db.backends.postgresql", "django_version": "6.0.6", "app_url": "https://heartlock.example.com" } }
Users
- GET /api/v1/admin/users/
List all users. Supports
?search=query parameter.
- PATCH /api/v1/admin/users/(int: pk)/
Toggle
is_superuser,is_staff, oris_activeon a user.Request body:
{"is_active": false}
You cannot remove your own superuser status or deactivate your own account. You cannot remove the last superuser.
- DELETE /api/v1/admin/users/(int: pk)/
Delete a user account. You cannot delete your own account.
Projects
- GET /api/v1/admin/projects/
List all projects. Supports
?search=query parameter.
- GET /api/v1/admin/projects/(uuid: pk)/
Retrieve a project with aggregate counts (members, environments, secrets).
- PATCH /api/v1/admin/projects/(uuid: pk)/
Update a project’s metadata (
name,description).
- DELETE /api/v1/admin/projects/(uuid: pk)/
Delete a project and all its associated data.
Members
- GET /api/v1/admin/projects/(uuid: project_id)/members/
List members of a project.
- POST /api/v1/admin/projects/(uuid: project_id)/members/
Add or update a member on a project.
Request body:
{"email": "user@example.com", "role": "member"}
- PATCH /api/v1/admin/projects/(uuid: project_id)/members/(int: user_id)/
Change a member’s role. Cannot demote the last owner.
Request body:
{"role": "admin"}
- DELETE /api/v1/admin/projects/(uuid: project_id)/members/(int: user_id)/
Remove a member. Cannot remove the last owner.
Invitations
- GET /api/v1/admin/invitations/
List all invitations across the instance. Supports
?status=pendingor?status=accepted.
- GET /api/v1/admin/projects/(uuid: project_id)/invitations/
List all invitations for a single project.
- DELETE /api/v1/admin/invitations/(uuid: pk)/
Revoke a pending invitation by id.
OIDC configuration
- GET /api/v1/admin/oidc/
Read the current OIDC SSO configuration. The
client_secretis masked on read.
- PATCH /api/v1/admin/oidc/
Update OIDC settings. Changes are persisted to
appconfig.jsonand take effect immediately (no restart required).Request body:
{ "enabled": true, "client_id": "heartlock", "client_secret": "...", "authorization_endpoint": "https://idp.example.com/auth", "token_endpoint": "https://idp.example.com/token", "userinfo_endpoint": "https://idp.example.com/me", "scopes": "openid email profile", "create_unknown_user": true }
- POST /api/v1/admin/oidc/test/
Probe the configured OIDC endpoints for reachability and return a connectivity report.
Response:
{ "reachable": true, "authorization_endpoint": true, "token_endpoint": true, "userinfo_endpoint": true, "discovery_url": "https://idp.example.com/.well-known/openid-configuration", "errors": [] }
OIDC SSO flow
These endpoints are at /oidc/ (not under /api/v1/).
- GET /oidc/authenticate/
Redirect the user to the IdP authorization endpoint. Accepts an optional
?next=/query parameter for post-login redirect.Requires
OIDC_ENABLEto be true and all required IdP endpoints to be configured. Returns 503 if SSO is not configured.
- GET /oidc/callback/
OIDC callback: exchanges the authorization code for tokens, retrieves the userinfo, and signs the user in. If
OIDC_CREATE_UNKNOWN_USERis true (default), a local account is created for unknown emails; otherwise returns 403.
- GET /oidc/logout/
Sign out locally and, if
OIDC_END_SESSION_ENDPOINTis configured, redirect to the IdP’s end-session endpoint.