API Reference

Base URL: {APP_URL}/api/v1. These are the exact endpoints the klenua desktop app itself calls to sign in, sync a project's state, and upload backups — not a general-purpose public API. Everything is optional: the app works fully signed out.

Response envelope

Success:

{ "success": true, "data": { } }

Error:

{ "success": false, "error": { "code": "INVALID_CREDENTIALS", "message": "Incorrect email or password." } }

error.code is a stable, machine-readable string — branch on it, not on the HTTP status or the message text. A validation error additionally includes error.details, a map of field → array of messages.

Authentication

POST /auth/login

FieldTypeRequiredNotes
emailstringyes
passwordstringyesNever stored by the app.
stay_signed_inbooleannoWith client_name set to klenua build, issues a refresh token that lasts 30 days instead of a normal session.
client_namestringnoMax 80 characters. Labels the session.
{
    "success": true,
    "data": {
        "user": { "id": 1, "username": "jane", "display_name": "Jane Doe", "email": "jane@example.com", "role": "user", "avatar_path": null },
        "access_token": "<jwt>",
        "refresh_token": "<opaque>"
    }
}

Send the access token on authenticated requests:

Authorization: Bearer <access_token>

The access token is short-lived. When a request returns 401, exchange the refresh token for a new pair and retry once.

POST /auth/refresh — body { "refresh_token": "<opaque>" }. Returns the same user, access_token, refresh_token shape as login. The refresh token is rotated, so store the new one.

POST /auth/logout — body { "refresh_token": "<opaque>" }. Revokes that refresh token. Always succeeds.

Changing or resetting your password on the website signs out every session, including the desktop app.

Syncing a project

POST /build/projects — requires a bearer token.

FieldTypeRequiredNotes
local_idstringyesA SHA-256 digest (64 lowercase hex characters) of the project's local folder path. Never the path itself.
project_namestringyesMax 160 characters.
project_typestringnoe.g. flutter, android, ios. Max 120 characters.
app_namestringnoThe app's display name. Max 160 characters.
bundle_idstringnoBundle ID / application ID. Max 255 characters.
versionstringnoMax 80 characters.
buildstringnoBuild number. Max 80 characters.
platformsstring[]noUp to 12 entries, each up to 40 characters.
source_of_truthstringnoWhich file the version was read from. Max 255 characters.

This is an upsert — syncing the same local_id again updates the existing record instead of creating a duplicate. A signed-in user can see everything they've synced at /account/synced on the website.

{ "success": true, "data": { "message": "Project synced to your klenua account." } }

GET /build/projects — requires a bearer token. Returns the signed-in user's synced projects, most recently synced first. Each record has local_id, project_name, project_type, app_name, bundle_id, version, build_number, platforms, source_of_truth, and synced_at. The app uses local_id and synced_at to show when a project was last synced.

Uploading backups

POST /build/projects/backups — requires a bearer token. Sent as multipart/form-data.

FieldTypeRequiredNotes
local_idstringyesSame digest as above.
relative_pathstringyesThe file's path inside the backup folder, e.g. android/app/build.gradle. 1–500 characters, letters, digits, ., _, -, space and / only; no leading /, no ...
backupfileyesThe backup file itself. Max 1 MB.

Accepted file types: gradle, java, json, kt, kts, pbxproj, plist, properties, toml, xml, yaml, yml. Uploading the same relative_path for the same project replaces the previous backup. Returns 201 with { "message": "Backup file uploaded." }. Uploaded backups appear next to the project at /account/synced and can be downloaded from there.

Versioning

There is no /v2 yet. A breaking change to this API would ship as a new version prefix, not a silent change to /v1.

Couldn't find the desktop app

If it's already installed, this can happen the first time your browser asks for permission to open it — try the button again. Otherwise, download it below and Get will open plugins directly from then on.

Download for macOS