# Lensdrop developer docs

Lensdrop is a photo delivery tool for photographers, not a developer platform. Its one public API is this read-only content API: every marketing page as markdown, an llms.txt index, and the discovery files agents expect. It needs no key, no account, and no registration. Uploads, payments, and gallery delivery have no third-party API; see "What is not public" below.

## Discovery files

- [/llms.txt](https://lensdrop.app/llms.txt) (text/markdown): Agent-readable index of Lensdrop's public content (llmstxt.org).
- [/llms-full.txt](https://lensdrop.app/llms-full.txt) (text/markdown): Every public page as one markdown document.
- [/openapi.json](https://lensdrop.app/openapi.json) (application/openapi+json): OpenAPI 3.1 description of the content API, with operationIds, typed errors, and headers.
- [/.well-known/api-catalog](https://lensdrop.app/.well-known/api-catalog) (application/linkset+json): RFC 9727 API catalog pointing at the spec and the docs.
- [/auth.md](https://lensdrop.app/auth.md) (text/markdown): Auth.md: how (and whether) agents authenticate. Short answer: anonymous.
- [/developers.md](https://lensdrop.app/developers.md) (text/markdown): This page, as markdown.
- [/sitemap.xml](https://lensdrop.app/sitemap.xml) (application/xml): Every indexable page.
- [/robots.txt](https://lensdrop.app/robots.txt) (text/plain): Crawl rules. AI crawlers are allowed by name; galleries and the signed-in app are not crawlable.

## Quickstart

Read the index:

```bash
curl https://lensdrop.app/llms.txt
```

Fetch a page as markdown by URL:

```bash
curl https://lensdrop.app/pricing.md
```

Or by content negotiation on the HTML URL:

```bash
curl -H "Accept: text/markdown" https://lensdrop.app/pricing
```

Get the OpenAPI description:

```bash
curl https://lensdrop.app/openapi.json
```

See an error (typed, RFC 9457):

```bash
curl -i https://lensdrop.app/blog/no-such-post.md
```

## Endpoints

All operations are anonymous GETs on https://lensdrop.app. Every markdown page is also reachable by requesting its HTML URL with `Accept: text/markdown`.

| Operation | operationId | Summary | Content type |
| --- | --- | --- | --- |
| GET /llms.txt | getLlmsTxt | Index of Lensdrop's agent-readable content | text/markdown |
| GET /llms-full.txt | getLlmsFullTxt | The full agent-readable corpus in one document | text/markdown |
| GET /index.md | getHomeMarkdown | What Lensdrop is and how delivery works | text/markdown |
| GET /pricing.md | getPricingMarkdown | Lensdrop pricing: tiers, add-ons, free tier, billing | text/markdown |
| GET /download.md | getDownloadMarkdown | Lensdrop desktop uploader | text/markdown |
| GET /help.md | getHelpMarkdown | Lensdrop help and FAQ | text/markdown |
| GET /use-cases.md | getUseCasesMarkdown | Lensdrop use cases, all niches in one document | text/markdown |
| GET /compare.md | getCompareMarkdown | Lensdrop compared with other gallery tools, all in one document | text/markdown |
| GET /blog.md | getBlogIndexMarkdown | Lensdrop blog index | text/markdown |
| GET /developers.md | getDeveloperDocsMarkdown | Lensdrop developer docs | text/markdown |
| GET /use-cases/{slug}.md | getUseCaseMarkdown | One use case as markdown | text/markdown |
| GET /compare/{slug}.md | getComparisonMarkdown | One comparison as markdown | text/markdown |
| GET /blog/{slug}.md | getBlogPostMarkdown | One blog post as markdown | text/markdown |
| GET /help/{slug}.md | getHelpGuideMarkdown | One help guide as markdown | text/markdown |
| GET /openapi.json | getOpenApiSpec | OpenAPI 3.1 description of this API | application/openapi+json |
| GET /.well-known/api-catalog | getApiCatalog | RFC 9727 API catalog | application/linkset+json |
| GET /auth.md | getAuthMd | How agents authenticate (they don't) | text/markdown |

The full OpenAPI 3.1 description, with parameter schemas and typed errors, is at [/openapi.json](https://lensdrop.app/openapi.json).

## Errors

Every error is an RFC 9457 problem details object served as `application/problem+json`, with a real HTTP status (never a 200 wrapping an error, never an HTML page for an API path). Fields: `type` (documentation URL for the code), `title`, `status`, `code` (stable, machine-readable), `detail`, `hint` (what to do next), optional `instance` (the request path). `error` is a legacy alias of `detail`.

Example:

```json
{
  "type": "https://lensdrop.app/developers#not_found",
  "title": "Not found",
  "status": 404,
  "code": "not_found",
  "detail": "No markdown twin at /blog/no-such-post.md.",
  "hint": "Start from /llms.txt for the agent-readable site index, /openapi.json for the API description, or /sitemap.xml for every page.",
  "instance": "/blog/no-such-post.md",
  "error": "No markdown twin at /blog/no-such-post.md."
}
```

Error codes:

| Status | Code | Title | What to do |
| --- | --- | --- | --- |
| 400 | bad_request | Bad request | Check the request body and parameters against the documented schema. |
| 400 | unsupported_version | Unsupported API version | Send a supported value in the API-Version header, or omit the header to use the current version. |
| 401 | unauthorized | Unauthorized | The credential is missing, malformed, or revoked. The public content API needs no credential; other surfaces are not open to third parties. |
| 403 | forbidden | Forbidden | The credential is valid but does not cover this resource. |
| 404 | not_found | Not found | Start from /llms.txt for the agent-readable site index, /openapi.json for the API description, or /sitemap.xml for every page. |
| 405 | method_not_allowed | Method not allowed | The content API is read-only: use GET or HEAD. |
| 406 | not_acceptable | Not acceptable | This resource is served as text/markdown or application/json. Adjust the Accept header. |
| 409 | conflict | Conflict | The resource is in a state that does not allow this change. Re-read it and retry if appropriate. |
| 413 | payload_too_large | Payload too large | Reduce the request size; uploads go directly to storage in parts, never through this API. |
| 429 | rate_limited | Too many requests | Wait for the number of seconds in Retry-After, then retry. The RateLimit and RateLimit-Policy headers describe the quota. |
| 500 | internal_error | Internal error | Retry with backoff. If it persists, contact support with the instance path. |
| 502 | bad_gateway | Upstream error | A dependency failed. Retry with backoff. |
| 503 | service_unavailable | Service unavailable | Retry after a short delay; honor Retry-After when present. |

Unknown paths anywhere on the site return a real 404. Browsers get the HTML page, `/api/*` paths and JSON accepts get a problem document, and everything else (curl, agents) gets a markdown body with links to the discovery files above.

## Rate limits

Anonymous access is limited per client IP to 120 requests per 60 seconds (sliding window), described by the IETF RateLimit header fields on every response. Exceeding it returns 429 `rate_limited` with `Retry-After`. Read `RateLimit` to self-throttle instead of retrying blindly.

- API-Version: The API version that produced the response. Currently 1. Example: `1`
- RateLimit-Policy: The quota policy (IETF RateLimit header fields): 120 requests per 60 seconds per client, sliding window. Example: `"content-api";q=120;w=60`
- RateLimit: Live quota state: r is the requests remaining in the window, t the seconds until it resets. Present whenever the limiter ran. Example: `"content-api";r=119;t=60`
- Retry-After: On a 429 only: seconds to wait before retrying. Example: `12`
- Deprecation: Only on responses from a deprecated API version (RFC 9745): the date the deprecation was announced. Example: `@1767225600`
- Sunset: Only on responses from a deprecated API version (RFC 8594): the date it stops being served. Example: `Wed, 01 Jul 2026 00:00:00 GMT`

## Versioning and deprecation

The content API is versioned by the API-Version header. Every response states the version that produced it (currently 1). Requests may pin a version with the same header; omit it to get the current version. A pinned version this deployment no longer serves answers 400 with code unsupported_version and the supported list.

URLs and response shapes within a version only change additively: new fields, new endpoints, new headers. Removing or renaming anything means a new version.

When a version is deprecated, responses from it carry Deprecation (RFC 9745) and Sunset (RFC 8594) headers and a Link with rel="deprecation" pointing at the notice on this page, for at least 6 months before the sunset date. Deprecations are also listed in llms.txt.

No version has been deprecated yet.

## Put a gallery on your website

Any active gallery can be shown inside a page on the photographer's own site with one copy-paste iframe snippet, no code and no API.

1. Turn the embed on. Open the gallery, then Settings, then Website embed, and turn it on. A snippet appears.
2. Paste the snippet. Copy the snippet and paste it where the gallery should appear: a code or HTML block on Squarespace, Wix, or WordPress, an embed block on Notion, or any HTML page.
3. Set the height. Change the height value in the snippet to taste. The frame scrolls on its own inside the page.

How it works: the snippet is a plain iframe pointing at a second, unguessable link separate from the share link, so it can be turned off or replaced without touching the link clients already have. Inside the frame the gallery behaves exactly like the share link: the PIN is asked for in the frame with the same attempt limits, and watermark, downloads, sections, Live Mode, and the six-month window all apply. Turning the embed off or generating a new embed link stops every page using the old snippet; the share link is unaffected.

## Questions

### Does Lensdrop have an API?

No. Lensdrop has no API for galleries, uploads, downloads, or billing. The only public API is a read-only content API that serves the marketing pages as markdown for AI agents. To show a gallery on a website, use the embed snippet instead.

### Can I show a Lensdrop gallery on my own website?

Yes. Every active gallery has a Website embed switch in its Settings. Turn it on, copy the iframe snippet, and paste it into any page that accepts HTML. Photos stay on Lensdrop.

### Does the PIN protect an embedded gallery?

Yes. An embedded gallery with a PIN asks visitors for it inside the frame, exactly as the share link does, with the same attempt limits. Turn the PIN off in the gallery's settings if the gallery is meant to be open on that page.

### Which website builders work with the Lensdrop embed?

Any builder that accepts an iframe: a code block on Squarespace, an HTML embed on Wix, a Custom HTML block on WordPress, an embed block on Notion, or a plain HTML page. No plugin is needed.

### Can clients download photos from an embedded gallery?

Downloads inside the embed follow the gallery's own download switch. With the watermark on, clients get watermarked copies and no bulk ZIP. With the watermark off, they get clean originals and the ZIP, exactly as on the share link.

### What happens to an embedded gallery when it expires?

Every Lensdrop gallery has a six-month active window that can be extended from the dashboard. After it ends, the embed shows a quiet 'not available' card instead of broken images. Turning the embed off or generating a new embed link does the same for the old snippet; the share link is unaffected.

### Can AI agents read Lensdrop's documentation?

Yes. Every marketing page has a markdown twin at the same URL with .md appended, an llms.txt index lists them, and openapi.json describes the content API. Access is anonymous and rate limited.

## What is not public

- Gallery uploads, downloads, and the client gallery viewer: no third-party API. Bytes move between browsers, storage, and an image worker through signed URLs, never through an app server that an integration could call.
- Billing: photographers pay Lensdrop through Dodo Payments (merchant of record; INR in India, USD elsewhere). There is no API to create purchases and there are never client-to-photographer payments.
- Connected devices: the desktop uploader and camera FTP use per-device credentials that the app issues and can revoke. They are not API keys for general use and are not documented here.

## Crawling and AI use

robots.txt allows search engines and AI crawlers by name (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, and others) on the public pages and publishes Content Signals permitting search, AI input, and AI training. Galleries (/g/), the signed-in app, and /api are not crawlable.

## Contact

hello@lensdrop.app
