@lovable.dev/sdk
v7.1.3
Published
TypeScript SDK for the Lovable API
Readme
@lovable.dev/sdk
TypeScript SDK for the Lovable API.
Warning: experimental software. This SDK is not covered by the public API v1 stability guarantees. Any release, including minor and patch versions, may change or remove methods, types, and behavior. Pin an exact version. For production integrations, call the public API directly.
Installation
npm install @lovable.dev/sdkCreate an API key
Keys are created in the workspace settings at https://lovable.dev/settings/api-keys
(the Access tokens tab). You need the admin or owner role in the workspace.
| Setting | Effect |
| --- | --- |
| Workspace | A key belongs to one workspace and can only read or change that workspace. Resources in other workspaces return 404. |
| Plan | The workspace must be on Business or higher; every public v1 call from a lower plan returns 402 payment_required. Two operations (project PII labels, Security Center project inventory) require Enterprise. |
| Access | Pick Read-only or Full access per resource. Projects maps to projects:read / projects:write, Workspace maps to workspaces:read / workspaces:write. A write scope includes the matching read scope. |
Every key from this page carries the public:v1 audience: it reaches the
advertised public v1 management operations and nothing else. The audience is
not shown in the settings page.
Quick start
import { LovableClient } from "@lovable.dev/sdk";
const client = new LovableClient({ apiKey: process.env.LOVABLE_API_KEY! });
const me = await client.me();
console.log(me.email, me.name);
// GET /v1/workspaces: list envelope (`data`, `pagination`) plus `workspaces`, an alias of `data`.
const { workspaces } = await client.listWorkspaces();
const workspaceId = workspaces[0].id;
// GET /v1/projects?workspace_id=...: same envelope, plus `projects` as an alias of `data`.
const page = await client.listProjects(workspaceId, { limit: 20 });
for (const project of page.data ?? []) {
console.log(project.id, project.name, project.is_published ? "Published" : "Not published");
}Every hand-written method returns the response body unchanged, apart from the
aliases noted above. client.typed is an openapi-fetch
client over the generated route types for any advertised operation without a
hand-written method.
First-party clients targeting a baseUrl ending in /internal use
client.internalTyped. It preserves the internal collaborator and member-sort
names. Both accessors reject a base URL for the
other surface; convenience methods support either base URL.
Pagination
List operations take limit (1 to 100, default 50) and cursor, and return
data with pagination.next_cursor and pagination.has_more. Keep the same
filters across pages.
let cursor: string | undefined;
do {
const page = await client.listProjects(workspaceId, { limit: 100, cursor });
for (const project of page.data ?? []) console.log(project.id);
cursor = page.pagination.has_more ? (page.pagination.next_cursor ?? undefined) : undefined;
} while (cursor);Create an embed URL
client.createEmbedUrl(projectId, parentOrigin) calls
POST /v1/projects/{project_id}/embed-url with scope projects:write. It
requires Business or higher and edit permission on the project. The response
contains a preview URL valid for one hour and bound to one exact HTTPS parent
origin. Visitors can view the built preview without a Lovable login.
Publish a project and poll the deployment
POST /v1/projects/{project_id}/publish (scope projects:write) returns 202
with a Deployment: id, status: "running", and null url, error_message,
and error_class. The polling response uses the same schema; url is set only
when that deployment completes. Poll GET /v1/projects/{project_id}/publish/{deployment_id}
(scope projects:read) no more than once every 2 seconds. completed and
error are terminal; keep polling on running and on unknown (a degraded,
non-terminal state). Give up after a deadline; the example uses 10 minutes.
const { data: accepted } = await client.typed.POST("/v1/projects/{project_id}/publish", {
params: { path: { project_id: projectId } },
body: {}, // optional: { audience: "workspace" }
});
if (!accepted?.id) throw new Error("publish was not accepted");
const deadline = Date.now() + 10 * 60 * 1000;
let deployment;
do {
if (Date.now() > deadline) throw new Error(`deployment ${accepted.id} did not finish in 10 minutes`);
await new Promise((resolve) => setTimeout(resolve, 2000));
({ data: deployment } = await client.typed.GET("/v1/projects/{project_id}/publish/{deployment_id}", {
params: { path: { project_id: projectId, deployment_id: accepted.id } },
}));
} while (deployment?.status === "running" || deployment?.status === "unknown");
if (deployment?.status === "completed") console.log(deployment.url);
else console.error(deployment?.error_class, deployment?.error_message);PATCH /v1/projects/{project_id}/publish updates audience and optional
audience_targets without deploying, and returns the resolved audience and
audience_targets. Read project GET for current is_published,
publish_audience, and publish_audience_targets; the published URL is the
url of the deployment that published it. An older version can remain
published while a new deployment runs or fails. Use DELETE on the publish path
to take the app offline.
Errors
Non-2xx responses throw ApiError. The API returns one error envelope on
every operation:
{
"type": "invalid_argument",
"title": "Invalid argument",
"status": 400,
"request_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"detail": "validation failed",
"errors": [{ "location": "query.limit", "message": "expected integer <= 100" }]
}type, title, status, and request_id are always present. detail,
errors[], and props are optional. Branch on type; title wording can
change.
| Envelope field | On ApiError |
| --- | --- |
| status | status |
| type | type |
| title | message |
| detail | detail |
| props | props |
| request_id, errors[] | Not exposed by the SDK. Both are in the response body when you call the API directly. |
import { ApiError } from "@lovable.dev/sdk";
try {
await client.getProject(projectId);
} catch (err) {
if (err instanceof ApiError && err.type === "project_not_found") return null;
throw err;
}Types you will meet first:
| status | type | Cause |
| --- | --- | --- |
| 401 | unauthorized | Missing, revoked, or expired key |
| 402 | payment_required | Workspace plan below Business (or Enterprise where required) |
| 403 | insufficient_scope | Key lacks the operation's scope |
| 403 | forbidden | Caller lacks the workspace or project permission |
| 403 | audience_forbidden | A public:v1 key called a route outside the public v1 contract |
| 404 | project_not_found, workspace_not_found | Missing resource, or a resource in another workspace |
| 406 | not_acceptable | Accept header excludes application/json |
| 429 | rate_limited | Rate limit hit; see below |
Rate limits
Responses carry X-RateLimit-Limit, X-RateLimit-Remaining, and, when
known, X-RateLimit-Reset (Unix seconds). Limits use a sliding window: per
API key for key-authenticated requests, per user for session and OAuth
requests. Publishing is limited separately (10 per minute by default), as are
deployment status polling (120 per minute), analytics, security-center
insights, and collaborator reads (300 per minute).
On 429 the client retries the request up to three times, waiting 100 ms,
300 ms, then 500 ms, or longer when the Retry-After header asks for more.
After the last retry it throws ApiError with status: 429 and
rateLimit.retryAfterMs parsed from Retry-After.
if (err instanceof ApiError && err.status === 429) {
await new Promise((resolve) => setTimeout(resolve, err.rateLimit?.retryAfterMs ?? 1000));
}Operations outside the public v1 contract
The client also exposes builder operations: project creation and remix
(createProject, remixProject), messages (chat, listMessages,
getMessage, waitForMessageCompletion, createVariant), database, git and
file access, uploads, knowledge, skills, connectors, folders, and
publish() (which calls the deprecated POST /v1/deployments alias). These
routes are not part of the public v1 contract. A key created in the settings
page receives 403 audience_forbidden from all of them. They require a key
without a public audience, they carry no stability guarantee, and they are not
documented here.
Workflows subpath
@lovable.dev/sdk/workflows defines compute-only durable workflows for the
Cloudflare runtime; it does not call the Lovable API.
import { defineWorkflow, toWorker } from "@lovable.dev/sdk/workflows";
const workflow = defineWorkflow<{ message: string }, { message: string }>("normalize", async (ctx) => ({
message: await ctx.step.run("normalize", () => ctx.input.message.trim()),
}));
export default toWorker(workflow);Reference
| Document | Content | | --- | --- | | API_SPEC.md | Every advertised public v1 operation with its scope, parameters, and response schema | | https://api.lovable.dev/v1/openapi.json | The served OpenAPI document; authoritative when the two disagree | | examples/httpie.md | The same calls over plain HTTP | | src/types.ts | Exported request and response types |
