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_days is optional; 13650 days. 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"
}

role is one of: owner, admin, member, viewer. Defaults to member.

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 run command.

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_key is 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, or is_active on 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=pending or ?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_secret is masked on read.

PATCH /api/v1/admin/oidc/

Update OIDC settings. Changes are persisted to appconfig.json and 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_ENABLE to 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_USER is true (default), a local account is created for unknown emails; otherwise returns 403.

GET /oidc/logout/

Sign out locally and, if OIDC_END_SESSION_ENDPOINT is configured, redirect to the IdP’s end-session endpoint.