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: .. code-block:: text Authorization: Bearer hlp_ 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 ------ .. http:get:: /api/v1/health/ Unauthenticated health check. **Example response**: .. code-block:: json {"status": "ok"} Authentication endpoints ------------------------ .. http:post:: /api/v1/auth/login/ Authenticate with email and password. Sets a session cookie. **Request body**: .. code-block:: json { "email": "user@example.com", "password": "s3cret123" } **Success** (200): .. code-block:: json { "id": "uuid", "email": "user@example.com", "first_name": "", "last_name": "", "is_superuser": false, "has_password": true } **Failure** (401): .. code-block:: json {"detail": "Invalid email or password."} .. http:post:: /api/v1/auth/logout/ End the current session. *Requires authentication.* .. http:get:: /api/v1/auth/me/ Return the authenticated user's profile. *Requires authentication.* **Response**: .. code-block:: json { "id": "uuid", "email": "user@example.com", "first_name": "", "last_name": "", "is_superuser": false, "has_password": true, "sso_enabled": false } .. http: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**: .. code-block:: json { "first_name": "Jane", "last_name": "Doe" } .. http:post:: /api/v1/auth/password/ Change the authenticated user's password. *Requires a browser session.* **Request body**: .. code-block:: json { "current_password": "old-password", "new_password": "new-password" } **Failure** (400): .. code-block:: json {"current_password": "Current password is incorrect."} Service tokens -------------- .. http:post:: /api/v1/auth/cli-token/ Create a service token for CLI authentication. *Requires authentication.* The token does not expire by default. .. http:get:: /api/v1/auth/tokens/ List the authenticated user's active (non-revoked) service tokens. .. http:post:: /api/v1/auth/tokens/ Create a service token with optional name and expiry. *Requires authentication.* **Request body**: .. code-block:: json { "name": "CI pipeline", "expires_days": 90 } ``expires_days`` is optional; ``1``–``3650`` days. Without it, the token never expires. **Response** (201): .. code-block:: json { "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. .. http:delete:: /api/v1/auth/tokens/(uuid:pk)/ Revoke a service token. *Requires authentication.* **Response**: 204 No Content Projects -------- .. http:get:: /api/v1/projects/ List projects the authenticated user is a member of. **Response**: .. code-block:: json [ { "id": "uuid", "name": "Web App", "slug": "web-app", "description": "Main web application", "role": "admin", "created_at": "...", "updated_at": "..." } ] .. http:post:: /api/v1/projects/ Create a new project. The authenticated user becomes the owner. **Request body**: .. code-block:: json { "name": "My Project", "description": "Optional description" } **Response** (201): .. code-block:: json { "id": "uuid", "name": "My Project", "slug": "my-project", "description": "Optional description", "role": "owner", "created_at": "...", "updated_at": "..." } .. http:get:: /api/v1/projects/(uuid:pk)/ Retrieve a project. .. http:put:: /api/v1/projects/(uuid:pk)/ Update a project. Requires **admin** or **owner** role. **Request body**: same fields as create (``name``, ``description``). .. http:patch:: /api/v1/projects/(uuid:pk)/ Partially update a project. Requires **admin** or **owner** role. .. http:delete:: /api/v1/projects/(uuid:pk)/ Delete a project. Requires **owner** role. Project members --------------- .. http:get:: /api/v1/projects/(uuid:project_id)/members/ List members of a project. **Response**: .. code-block:: json [ { "id": 1, "user_id": "uuid", "email": "user@example.com", "first_name": "Jane", "last_name": "Doe", "role": "admin" } ] .. http:post:: /api/v1/projects/(uuid:project_id)/members/ Add or update a member. Requires **admin** or **owner** role. **Request body**: .. code-block:: json { "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. .. http: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 -------------------- .. http:get:: /api/v1/projects/(uuid:project_id)/invitations/ List pending invitations for a project. Requires **admin** or **owner** role. .. http:post:: /api/v1/projects/(uuid:project_id)/invitations/ Create an invitation. Requires **admin** or **owner** role. **Request body**: .. code-block:: json { "email": "invitee@example.com", "role": "member" } If the email already has a pending invitation for this project, returns 400. .. http:delete:: /api/v1/projects/(uuid:project_id)/invitations/(uuid:pk)/ Revoke a pending invitation. Requires **admin** or **owner** role. .. http: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. .. http:get:: /api/v1/invitations/(str:token)/ Public (unauthenticated) lookup of an invitation by its raw token. **Response**: .. code-block:: json { "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 } .. http: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): .. code-block:: json { "password": "new-password", "first_name": "Jane", "last_name": "Doe" } **Response** (201, new account): .. code-block:: json { "accepted": true, "created_account": true, "project_id": "uuid" } Environments ------------ .. http:get:: /api/v1/projects/(uuid:project_id)/environments/ List environments for a project. **Response**: .. code-block:: json [ {"id": "uuid", "name": "Development", "slug": "development"}, {"id": "uuid", "name": "Production", "slug": "production"} ] .. http:post:: /api/v1/projects/(uuid:project_id)/environments/ Create an environment. Requires **member** role or above (write access). **Request body**: .. code-block:: json {"name": "Staging"} .. http:get:: /api/v1/projects/(uuid:project_id)/environments/(uuid:pk)/ Retrieve an environment. .. http:put:: /api/v1/projects/(uuid:project_id)/environments/(uuid:pk)/ Update an environment. Requires write access. .. http:patch:: /api/v1/projects/(uuid:project_id)/environments/(uuid:pk)/ Partially update an environment. Requires write access. .. http:delete:: /api/v1/projects/(uuid:project_id)/environments/(uuid:pk)/ Delete an environment. Requires write access. Secrets ------- .. http:get:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/ List secrets in an environment. Values are returned in plaintext. **Response**: .. code-block:: json [ { "id": "uuid", "key": "DATABASE_URL", "value": "postgres://...", "comment": "Primary DB" } ] .. http:post:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/ Create a secret. Requires write access. **Request body**: .. code-block:: json { "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. .. http:get:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/(uuid:pk)/ Retrieve a single secret. .. http:put:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/(uuid:pk)/ Replace a secret. Requires write access. .. http:patch:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/(uuid:pk)/ Partially update a secret. Requires write access. .. http:delete:: /api/v1/projects/(uuid:project_id)/environments/(uuid:env_id)/secrets/(uuid:pk)/ Delete a secret. Requires write access. .. http: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**: .. code-block:: json [ {"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. .. http:get:: /api/v1/setup/status/ Report the current onboarding state. **Response** (onboarding required): .. code-block:: json { "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): .. code-block:: json {"onboarding_required": false} .. http:post:: /api/v1/setup/apply-config/ Write base URL, database URL, and encryption key to the config file (phase 1). **Request body**: .. code-block:: json { "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``. .. http: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**: .. code-block:: json {"restarting": true} .. http: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**: .. code-block:: json { "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 ^^^^^^^^ .. http:get:: /api/v1/admin/overview/ Aggregate instance statistics and read-only configuration. **Response**: .. code-block:: json { "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 ^^^^^ .. http:get:: /api/v1/admin/users/ List all users. Supports ``?search=`` query parameter. .. http:patch:: /api/v1/admin/users/(int:pk)/ Toggle ``is_superuser``, ``is_staff``, or ``is_active`` on a user. **Request body**: .. code-block:: json {"is_active": false} You cannot remove your own superuser status or deactivate your own account. You cannot remove the last superuser. .. http:delete:: /api/v1/admin/users/(int:pk)/ Delete a user account. You cannot delete your own account. Projects ^^^^^^^^ .. http:get:: /api/v1/admin/projects/ List all projects. Supports ``?search=`` query parameter. .. http:get:: /api/v1/admin/projects/(uuid:pk)/ Retrieve a project with aggregate counts (members, environments, secrets). .. http:patch:: /api/v1/admin/projects/(uuid:pk)/ Update a project's metadata (``name``, ``description``). .. http:delete:: /api/v1/admin/projects/(uuid:pk)/ Delete a project and all its associated data. Members ^^^^^^^ .. http:get:: /api/v1/admin/projects/(uuid:project_id)/members/ List members of a project. .. http:post:: /api/v1/admin/projects/(uuid:project_id)/members/ Add or update a member on a project. **Request body**: .. code-block:: json {"email": "user@example.com", "role": "member"} .. http:patch:: /api/v1/admin/projects/(uuid:project_id)/members/(int:user_id)/ Change a member's role. Cannot demote the last owner. **Request body**: .. code-block:: json {"role": "admin"} .. http:delete:: /api/v1/admin/projects/(uuid:project_id)/members/(int:user_id)/ Remove a member. Cannot remove the last owner. Invitations ^^^^^^^^^^^ .. http:get:: /api/v1/admin/invitations/ List all invitations across the instance. Supports ``?status=pending`` or ``?status=accepted``. .. http:get:: /api/v1/admin/projects/(uuid:project_id)/invitations/ List all invitations for a single project. .. http:delete:: /api/v1/admin/invitations/(uuid:pk)/ Revoke a pending invitation by id. OIDC configuration ^^^^^^^^^^^^^^^^^^ .. http:get:: /api/v1/admin/oidc/ Read the current OIDC SSO configuration. The ``client_secret`` is masked on read. .. http:patch:: /api/v1/admin/oidc/ Update OIDC settings. Changes are persisted to ``appconfig.json`` and take effect immediately (no restart required). **Request body**: .. code-block:: json { "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 } .. http:post:: /api/v1/admin/oidc/test/ Probe the configured OIDC endpoints for reachability and return a connectivity report. **Response**: .. code-block:: json { "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/``). .. http: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. .. http: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. .. http:get:: /oidc/logout/ Sign out locally and, if ``OIDC_END_SESSION_ENDPOINT`` is configured, redirect to the IdP's end-session endpoint.