# Add an item to a folder Source: https://uselora.dev/api-reference/folders/add-an-item-to-a-folder /openapi.json post /v1/folders/{id}/items Adds an existing item to this folder as an **additional** appearance. The item's primary folder is unchanged — to move it, `PATCH /v1/shortcuts/{handle}` with a new `folderId`. Adding an item to a folder never changes who can see that item: visibility lives on the item, and a folder only filters. Idempotent — adding an item that already appears in the folder succeeds without creating a duplicate. An item may appear in at most 20 additional folders. # Create folder Source: https://uselora.dev/api-reference/folders/create-folder /openapi.json post /v1/folders Creates a folder in the workspace. Folder names must be unique within the workspace. # Delete folder Source: https://uselora.dev/api-reference/folders/delete-folder /openapi.json delete /v1/folders/{id} Deletes a folder. Re-deleting an already-deleted folder returns `not_found`. The workspace's default folder cannot be deleted, and folders that still contain shortcuts return `folder_not_empty`. # List folders Source: https://uselora.dev/api-reference/folders/list-folders /openapi.json get /v1/folders Returns folders in the workspace, ordered newest-first, paginated by an opaque cursor. Deleted folders are excluded. # Remove an item from a folder Source: https://uselora.dev/api-reference/folders/remove-an-item-from-a-folder /openapi.json delete /v1/folders/{id}/items/{itemType}/{itemId} Removes an item's **additional** appearance from this folder. The item itself is not deleted and keeps its primary folder. An item whose primary folder is this folder cannot be removed this way — it would be left without a canonical location; move it instead. # Retrieve folder Source: https://uselora.dev/api-reference/folders/retrieve-folder /openapi.json get /v1/folders/{id} Returns a folder by its ID. # Update folder Source: https://uselora.dev/api-reference/folders/update-folder /openapi.json patch /v1/folders/{id} All fields are optional. Omitted fields stay as-is. Renaming a folder updates its slug and runs the same uniqueness check as create. The workspace's default folder cannot be edited. # Retrieve QR code Source: https://uselora.dev/api-reference/qr-codes/retrieve-qr-code /openapi.json get /v1/qr Generates a PNG image of a QR code for the given URL. Accepts both anonymous and Bearer-authenticated callers; anonymous responses encode a `lora-links.com` redirect URL, authenticated responses encode the destination verbatim and may use `hideLogo` on Basic / Business / Enterprise plans. # Create shortcut Source: https://uselora.dev/api-reference/shortcuts/create-shortcut /openapi.json post /v1/shortcuts Creates a shortcut in the workspace. # Delete shortcut Source: https://uselora.dev/api-reference/shortcuts/delete-shortcut /openapi.json delete /v1/shortcuts/{handle} Deletes a shortcut by handle. The handle is either the shortcut's `slug` or its `id` (UUID). The slug becomes immediately reusable for a new shortcut. Idempotent — re-deleting an already-deleted shortcut returns `not_found`. # List shortcuts Source: https://uselora.dev/api-reference/shortcuts/list-shortcuts /openapi.json get /v1/shortcuts Returns shortcuts visible to the caller, paginated by an opaque cursor. Most-used first by default; use `sort` and `dir` to re-order, and the `tags` / `creator` / `owner` / `expired` / `passwordProtected` / `masked` parameters to filter. Deleted shortcuts are always excluded; archived ones unless `archived=true`; expired ones unless `showExpired=true` (subject to role/ownership — moderators and above see every expired shortcut they can access, other members only their own). The query parameters mirror the in-app overview's own URL contract, so a link a user shares out of the app can be pasted here unchanged. Boolean parameters accordingly accept `1` / `0` as well as `true` / `false`; any other value is a `400 validation_error`. A cursor encodes a position in one specific ordering. Changing any ordering or filter parameter invalidates it — start the new query without a cursor rather than replaying the previous page's `nextCursor`, which returns `400 invalid_cursor`. # Retrieve shortcut Source: https://uselora.dev/api-reference/shortcuts/retrieve-shortcut /openapi.json get /v1/shortcuts/{handle} Returns a shortcut by its handle. The handle is either the shortcut's `slug` or its `id` (UUID). Archived shortcuts are returned. # Update shortcut Source: https://uselora.dev/api-reference/shortcuts/update-shortcut /openapi.json patch /v1/shortcuts/{handle} All fields are optional. Omitted fields stay as-is. Renaming the slug runs the same reserved-word and uniqueness checks as create. `archived` is not editable here and will get its own endpoint. # Upsert shortcut Source: https://uselora.dev/api-reference/shortcuts/upsert-shortcut /openapi.json put /v1/shortcuts/{handle} Creates a shortcut at the given slug, or updates the existing one. Returns `201` when a new shortcut is created and `200` when an existing one is updated. The handle is taken verbatim as the slug — for symmetry with `GET`/`PATCH`/`DELETE` the segment is named `{handle}`, but PUT does not resolve UUID-shaped handles to a row's ID. One guardrail: a UUID-shaped handle that collides with an existing shortcut's `id` returns `400 validation_error` (use `PATCH /v1/shortcuts/{handle}` to update by ID); UUID-shaped slugs that don't collide are accepted as slugs. # Create tag Source: https://uselora.dev/api-reference/tags/create-tag /openapi.json post /v1/tags Creates a new tag in the specified workspace. # Delete tag Source: https://uselora.dev/api-reference/tags/delete-tag /openapi.json delete /v1/tags/{id} Soft-deletes the tag. Idempotent — re-deleting an already-deleted tag returns `not_found`. # List tags Source: https://uselora.dev/api-reference/tags/list-tags /openapi.json get /v1/tags Returns tags in the workspace, ordered newest-first, paginated by an opaque cursor. Deleted tags are excluded. # Retrieve tag Source: https://uselora.dev/api-reference/tags/retrieve-tag /openapi.json get /v1/tags/{id} Returns a tag by its ID. # Update tag Source: https://uselora.dev/api-reference/tags/update-tag /openapi.json patch /v1/tags/{id} All fields are optional. Renaming a tag updates its slug and runs the same uniqueness check as create. # Getting started Source: https://uselora.dev/essentials/getting-started Learn how to use Lora's API to manage resources in your Lora workspace programmatically. ## Endpoint The API is REST over HTTPS. Plain HTTP isn't accepted. ```text theme={null} https://api.uselora.com ``` ## Authentication Send your API key as a Bearer token on every request: ```text theme={null} Authorization: Bearer lora_sk_xxxx ``` ## API key ### Create an API key Open **Profile > API Keys** in your workspace. Click **Create** and pick the scopes this key should carry. Send the key as a Bearer token on every request: ```text theme={null} Authorization: Bearer lora_sk_xxxx ``` ## Scopes A key carries scopes that limit what it can do. Select only the scopes the integration needs to reduce the impact of an exposed key. | Scope | What it grants | | ----------------- | ------------------------------------------------------ | | `shortcuts.read` | Read shortcuts in the workspace. | | `shortcuts.write` | Create, update, and delete shortcuts in the workspace. | | `tags.read` | Read tags in the workspace. | | `tags.write` | Create, update, and delete tags in the workspace. | | `folders.read` | Read folders in the workspace. | | `folders.write` | Create, update, and delete folders in the workspace. | ## Errors Every error response is JSON with the same shape. `error` is human-readable. `requestId` correlates with the `X-Lora-Request-Id` response header; quote it when filing a support ticket. `traceId` is a trace identifier when one is available. `code` is optional, and when present it's a machine-readable handle you can branch on. Minimum shape: ```json theme={null} { "error": "Human-readable description of what went wrong.", "requestId": "fra1::iad1::abc123" } ``` With a machine-readable code: ```json theme={null} { "error": "One or more fields failed validation.", "code": "validation_error", "requestId": "fra1::iad1::abc123", "traceId": "0123456789abcdef0123456789abcdef" } ``` ### What a 401 means Missing, malformed, unknown, and expired keys return the same generic 401 body: ```json theme={null} { "error": "Unauthorized", "requestId": "fra1::iad1::abc123" } ``` There's no `code` field on the generic 401. A disabled key returns the same status with `code: "apikey_disabled"`. Treat any 401 as "your request wasn't authenticated" and check the key you sent. If the key is valid but lacks the scope an endpoint needs, you get `403` with `code: "insufficient_scope"` and a `required_scope` field naming what was missing. ### Common error codes The endpoint reference documents the responses for each operation. These are the shared and frequently returned machine-readable codes. New codes may be added. | Code | Status | When | | ----------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `validation_error` | 400 | Schema validation failed. The body includes `fieldErrors` keyed by field name. | | `invalid_json` | 400 | The request body wasn't valid JSON or wasn't a JSON object. | | `invalid_cursor` | 400 | A list cursor was malformed. | | `reserved_slug` | 400 | The submitted shortcut slug is reserved. | | `masked_requires_https` | 400 | A masked shortcut used a destination that isn't HTTPS. | | `masked_domain_blocked` | 400 | A masked shortcut used a destination that Lora doesn't allow for masking. | | `qr_data_too_long` | 400 | The payload exceeds the capacity of the requested QR error-correction level. | | `apikey_disabled` | 401 | The API key was disabled. | | `plan_requires_upgrade` | 402/403 | The workspace plan doesn't include API access or a requested capability. Anonymous QR logo removal returns 403; plan gates return 402. | | `quota_exceeded` | 402 | The workspace reached a plan limit for the resource being created. | | `insufficient_scope` | 403 | The key lacks the endpoint's required scope. The body includes `required_scope`. | | `no_permission` | 403 | The key owner is not allowed to access the resource or workspace. | | `system_folder` | 403 | The operation isn't allowed on a system folder. | | `not_found` | 404 | The requested resource wasn't found or isn't visible to the caller. | | `folder_not_found` | 404 | The requested primary folder wasn't found or isn't visible. | | `additional_folder_not_found` | 404 | An additional folder wasn't found or isn't visible. | | `tag_not_found` | 404 | A requested tag wasn't found or isn't visible. | | `slug_conflict` | 409 | A shortcut already uses the requested slug. | | `tag_name_exists` | 409 | A tag already uses the requested name. | | `folder_name_exists` | 409 | A folder already uses the requested name. | | `already_primary` | 409 | The shortcut already uses the target as its primary folder. | | `too_many_folders` | 409 | The shortcut has reached its additional-folder limit. | | `folder_not_empty` | 409 | The folder still contains shortcuts that prevent the operation. | | `rate_limit_exceeded` | 429 | The request budget is exhausted. See `Retry-After` and [Rate limits](/essentials/rate-limits). | | `internal_error` | 500 | The server failed unexpectedly. | ### Retry safety Retry idempotent operations with exponential backoff. A write that returns `500` may already have applied. Read the resource or otherwise reconcile its state before retrying the write. # Rate limits Source: https://uselora.dev/essentials/rate-limits How Lora throttles requests and what to do when you hit the cap. Rate limits are per key, not per user or per workspace. Use a separate key per integration so one integration cannot consume another's budget. ## Plan limits The workspace plan sets the budget for every active key: | Plan | Requests per key | Window | | ----------------------- | ---------------- | -------- | | Free | API unavailable | — | | Basic | 600 | 1 minute | | Business and Enterprise | 1,500 | 1 minute | The counter resets when its one-minute window rolls. Plan changes synchronize the cap on existing keys as well as new ones. Per-key overrides aren't available in the settings UI. ## When you hit the limit You get `429 Too Many Requests` with a `Retry-After` header in seconds: ```json theme={null} { "error": "Too many requests", "code": "rate_limit_exceeded", "requestId": "fra1::iad1::abc123" } ``` Wait the indicated number of seconds, then retry. Add jitter so concurrent workers do not retry at once: ```ts theme={null} async function callWithRetry(url: string, init: RequestInit, attempt = 0) { const response = await fetch(url, init); if (response.status !== 429 || attempt >= 4) { return response; } const retryAfter = Number(response.headers.get("retry-after") ?? "1"); const jitter = Math.random() * 500; await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter)); return callWithRetry(url, init, attempt + 1); } ``` ## Choosing a key budget A few guidelines: * For an interactive integration like an internal tool or one-user script, use the plan limit as-is. * For background jobs and batch operations, pace the worker fleet so the combined rate for one key stays below its plan limit. * Do not embed workspace API keys in browser extensions, public widgets, or other untrusted clients. Keep keys on a trusted server and proxy requests through your backend.