FindPreset API
The extraction behind findpreset.com, from your own code. Send a photo, get a Lightroom preset file back.
One endpoint does what the site does. When a photo still carries its Lightroom settings, the API reads them from the file and returns them as a preset. When it does not, AI looks at the photo and builds an AI-extracted Lightroom preset that recreates its look. A style match takes a reference photo and your own photo and builds a preset that moves yours toward the reference.
The API comes with Premium and uses the same credits as findpreset.com: an AI-extracted Lightroom preset costs 1 credit, an exact read a tenth of one. Exact extraction stays free on findpreset.com with no account. Refused requests, polls, file downloads and balance reads cost nothing. Create an API key in your account to start.
Build it with an AI agent
Working with an AI coding agent such as Claude Code, Cursor or GitHub Copilot? Copy this prompt into it. It gives the agent every endpoint, price and error code, and tells it to keep your key in FINDPRESET_API_KEY, never in the code, to mock the API in tests, and to ask you before any call that spends credits. The prompt holds no key, so it is safe to paste.
Read the prompt
# Task: add the FindPreset API to this project
FindPreset turns a photo into a Lightroom preset (an .xmp file). Add an integration with its API to this project, following the instructions below. Full reference: https://findpreset.com/developers
## Before you write code
1. Look at this project and work out its language, framework and where photos come from. Build the integration in that stack, in server-side code only. If it is not clear what I want (which operation, where the photos come from, where the presets should go), ask me before you start.
2. The API key: read it from the environment variable FINDPRESET_API_KEY. Never hard-code it, commit it, log it or print it, and never send it to a browser or ship it inside an app. Add FINDPRESET_API_KEY with an empty value to the project's committed env example file (such as .env.example; create one if there is none), make sure the file that will hold the real value (such as .env or .env.local) is ignored by git, and tell me where to set it. Never write a real key into any file yourself. I create keys at https://findpreset.com/account/api; the API comes with a FindPreset Premium account.
3. Calls spend my account's plan and bonus credits, never a credit bought on findpreset.com to unlock one preset: 1 credit for an AI-extracted preset or a style match, 0.1 credit for an exact read (1 credit opens 10 reads; exact extraction on findpreset.com itself stays free). Never call the live API from automated tests: mock the HTTP calls. Ask me before you make any live call that spends credits. GET /api/v1/balance is free, so it is the safe way to check that the key works.
## The API
Base URL: https://findpreset.com
Authentication: send the header "Authorization: Bearer <key>" (or "X-API-Key: <key>"). Keys start with fp_live_.
The API sends no CORS headers, so it cannot be called from a web page: call it from a server, a script or a background job.
### POST /api/v1/extractions
Send multipart/form-data with these fields:
- operation (required): exact, ai, compare or auto.
- exact: reads the Lightroom settings embedded in the photo. Send "image", or "xmp" (the settings packet or the text of a .xmp sidecar, up to 1 MB). Costs 0.1 credit, charged even when the photo holds no settings. Reads come 10 to a credit: the first read spends 1 whole credit and opens 10 reads, the next 9 use them, and a read that fails goes back to them. Reads left still work at 0 credits; GET /api/v1/balance shows them as exact_reads_left.
- ai: an AI-extracted preset from one photo. Send "image" and optionally "creativity" (a whole number from 0 to 100, default 50). Costs 1 credit.
- compare: a style match, a preset that moves the target photo toward the reference photo. Send "reference" and "target" images, optionally "creativity". Costs 1 credit.
- auto: exact when the photo carries settings, otherwise ai. Send "image", optionally "xmp" and "creativity". Costs 0.1 credit when settings are found, otherwise 1 credit.
- Header Idempotency-Key: send one on every POST, 1 to 255 printable ASCII characters with no spaces. Make one key per job, save it with the job (a random UUID is fine) and reuse it only to retry that same job, so a retry after a timeout or a dropped connection returns the first request instead of running and billing again. Running the same photo again on purpose (another creativity, or after a failed or expired request) needs a new key. Do not derive the key from the photo alone: a key stays bound to its first request for 90 days, so the same photo sent again later would get the old answer, or 410 result_expired once that result is erased. The same key with a different photo, xmp, operation or creativity answers 409 idempotency_key_reused. A request that ended failed stays failed for its key, and a 410 result_expired answer to a POST means that request's result is gone: to run the photo again, use a new key. Any other refusal (an error answer other than 409 and 410) never uses up a key.
- Query ?wait=true (optional): holds the connection open until an AI request finishes, up to about 270 seconds. Set the HTTP client timeout above 300 seconds when you use it. It can still answer 202, so handle 202 either way.
Answers:
- 200: finished. Check status (succeeded, not_found or failed), not only the HTTP code. Exact reads normally answer 200 at once, but any request can answer 202, so handle 202 everywhere.
- 202: still processing. The body is the extraction with status "processing". Poll GET /api/v1/extractions/{id} with the body's id (the Location header carries the same path, relative to the base URL); Retry-After says how many seconds to wait before polling.
- Answers to POST /api/v1/extractions usually carry X-Credits-Remaining (plan and bonus credits left) and X-Exact-Reads-Left, and GET /api/v1/balance always does. A replayed answer (an Idempotency-Key seen before) also carries Idempotent-Replayed: true.
### GET /api/v1/extractions/{id}
Poll until status is no longer "processing": 202 while it runs, 200 once it is succeeded, not_found (an exact read found no settings) or failed. Wait the Retry-After seconds between polls; an AI request usually finishes in under a minute. Polls are free. A request is never left processing much longer than 6 minutes: by then it has finished, or it is closed as failed (error.code timeout) and its credit given back, so stop polling and report to me after 12 minutes.
### GET /api/v1/extractions/{id}/preset.xmp
Downloads the preset as a file, the same content as result.preset_xmp. Add ?variant=masks for the version with local adjustments, when result.preset_with_masks_url is not null. Answers 409 result_not_ready while the request is processing. Needs the API key of the account that made the request. Free.
### GET /api/v1/balance
Free. Returns whether the account has API access, its credits, the exact reads it has left and the price of each operation.
## The extraction object
Fields: id, object ("extraction"), operation, method (what ran: exact, ai or compare), status (processing, succeeded, not_found or failed), credits (what it costs), created_at, completed_at, result_expires_at, result, error.
result, when status is succeeded:
- preset_xmp: the Lightroom preset file as a string. Write it to a file ending in .xmp; it imports in Lightroom (Presets panel, then Import Presets).
- preset_xmp_with_masks: the same preset with its local adjustments, or null.
- preset_url and preset_with_masks_url: where those files download, with the API key.
- settings: the preset's global settings as an object (for example Exposure, Contrast). New keys may be added.
- full_copy, software: exact reads only. engine_version: AI results only.
error, when status is not_found or failed: { code, message }. Codes:
- no_settings: An exact read found no Lightroom settings in the photo (status not_found). The read is charged (a tenth of a credit).
- overloaded: The AI was busy. The credit was given back.
- ai_error: The AI could not extract a preset from this photo. The credit was given back.
- parse_error: The AI answer could not be read as a preset. The credit was given back.
- timeout: The run did not finish in time. The credit was given back.
- internal_error: Our fault. The credit (or the exact read) was given back.
What to do with a request that ended without a preset (nothing was charged unless it says so):
- overloaded, timeout, internal_error: retry once, after a short wait, with a new Idempotency-Key (the old key keeps its failed answer).
- ai_error, parse_error: the photo is the likely cause. Do not retry it automatically; tell me. They count toward 429 failure_limit: an account with 10 or more of them in 24 hours, more than its successes, is paused for AI requests for up to 24 hours.
- no_settings: the exact read was charged and found nothing. Do not rerun it as ai without asking me (that costs 1 credit); auto does both in one request.
Results are erased 7 days after they finish (then 410 result_expired), so save the .xmp as soon as you get it.
## Files and limits
- Each image can be up to 4 MB, and a whole request up to 4.5 MB.
- AI (ai, compare, and auto when it falls back to AI) takes JPEG, PNG, WebP or TIFF, and reads photos at 1568 pixels on the long edge. Resize larger photos to 1568 pixels on the long edge before an AI request, as a JPEG at quality 90 or more, in sRGB or with its color profile kept: that is the size the AI reads, and it keeps a compare request (two images) under the size limit.
- Exact reads also take HEIC and DNG. Never resize or re-encode a photo for an exact read or for auto: that usually drops the settings. For an original over 4 MB, read its settings packet (the XMP) from the original and send that as "xmp": alone with operation exact, or with auto together with a copy resized for AI as "image" (auto always needs an image: the AI reads it when the packet holds no settings).
- An account runs up to 2 AI requests at a time; more answer 429 concurrency_limit. Queue AI work with at most 2 in flight.
## Errors
A refused request answers { "error": { "code": "...", "message": "..." } } and is never charged. Branch on code, never on message: codes are stable, messages may change.
- 401 missing_api_key: No API key was sent.
- 401 invalid_api_key: The key is wrong, revoked, or from another environment.
- 429 rate_limited: Too many requests with a wrong key from your address. Wait a minute.
- 400 bad_request: The body is not multipart/form-data, or a field has the wrong type.
- 400 missing_operation: No "operation" field.
- 400 unsupported_operation: The operation is not exact, ai, compare or auto.
- 400 missing_file: The operation needs an image the request does not have.
- 400 bad_creativity: "creativity" is not a whole number from 0 to 100.
- 400 bad_idempotency_key: The Idempotency-Key header is empty, too long or has spaces.
- 400 unreadable_image: The image could not be decoded.
- 402 insufficient_credits: Not enough credits for this request. Nothing was charged.
- 403 plan_required: The API comes with Premium, and this account does not have it. Nothing was charged.
- 404 not_found: No such request for this account, or no such endpoint.
- 404 no_preset: The request finished without a preset file (it failed, or the photo had no settings to read), or has no version with local adjustments.
- 405 method_not_allowed: The endpoint does not take this HTTP method.
- 409 idempotency_key_reused: This Idempotency-Key was used for a different request.
- 409 result_not_ready: The request is still processing, so its preset file is not ready yet. Retry after the Retry-After seconds.
- 410 result_expired: The request finished more than 7 days ago; its result was erased.
- 413 file_too_large: An image is over 4 MB, or "xmp" is over 1 MB.
- 413 request_too_large: The whole request is over 4.5 MB.
- 415 unsupported_file: A file is not an image we accept, or is a HEIC or raw file sent for AI.
- 429 concurrency_limit: Your account already has its maximum number of AI requests running.
- 429 failure_limit: Too many AI requests on this account failed in the last 24 hours. Nothing was charged; exact reads still work, and AI works again once those failures are more than 24 hours old.
- 500 internal_error: Our fault. A request that was not opened costs nothing; to check one that was, retry with the same Idempotency-Key, which never runs or bills twice.
- 503 capacity: The AI is at capacity across all customers. Retry after the Retry-After seconds.
How to handle them:
- Wait the Retry-After seconds, then retry the same request (same Idempotency-Key); after about 10 minutes of this, stop and show the message to the user: 429 rate_limited, 409 result_not_ready, 429 concurrency_limit, 503 capacity. Also a network error or timeout on a POST: retry it with the same Idempotency-Key.
- Retry with the same Idempotency-Key (it never runs or bills twice), up to 3 times with a growing wait (for example 5, 30 and 120 seconds), then stop and show the message to the user: 500 internal_error.
- Fix the request (the code, or the file it sends); do not send it again unchanged: 400 bad_request, 400 missing_operation, 400 unsupported_operation, 400 missing_file, 400 bad_creativity, 400 bad_idempotency_key, 405 method_not_allowed, 409 idempotency_key_reused, 413 file_too_large, 413 request_too_large, 415 unsupported_file.
- Do not retry: stop and show the message to the user: 401 missing_api_key, 401 invalid_api_key, 400 unreadable_image, 402 insufficient_credits, 403 plan_required, 404 not_found, 404 no_preset, 410 result_expired, 429 failure_limit.
- On 402 insufficient_credits, also tell me that plan credits come back when the plan renews, and that a bigger quota is a request to support@findpreset.com.
## Done means
- One small module in this project's language with a function that takes a photo (and the operation), sends it, waits or polls until it finishes, and returns the .xmp text or saves it to a file, handling every error above.
- Unit tests with mocked HTTP: an exact read, an AI request that answers 202 and then succeeds, an AI request that ends failed, and the 402 insufficient_credits and 429 concurrency_limit refusals.
- A short README section: which env var to set, and how to call the function.
- Then show me what you changed, and the one command that makes a real call, for me to run when I am ready.
Quickstart
This reads the photo's settings when it has them and otherwise runs AI, and waits for the answer:
curl "https://findpreset.com/api/v1/extractions?wait=true" \ -H "Authorization: Bearer $FINDPRESET_API_KEY" \ -H "Idempotency-Key: my-photo-0001" \ -F operation=auto \ -F image=@photo.jpg
The answer is an extraction. Save result.preset_xmp as a .xmp file, or download the file from result.preset_url with the same key, and import it in Lightroom (Presets panel, then Import Presets).
{
"id": "6f1c2a9e-3b7d-4c55-9a0e-2d8f61b4c7aa",
"object": "extraction",
"operation": "auto",
"method": "ai",
"status": "succeeded",
"credits": 1,
"created_at": "2026-10-16T09:12:03.120Z",
"completed_at": "2026-10-16T09:12:51.884Z",
"result_expires_at": "2026-10-23T09:12:51.884Z",
"result": {
"preset_xmp": "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"> ... </x:xmpmeta>",
"preset_xmp_with_masks": null,
"preset_url": "https://findpreset.com/api/v1/extractions/6f1c2a9e-3b7d-4c55-9a0e-2d8f61b4c7aa/preset.xmp",
"preset_with_masks_url": null,
"settings": { "Exposure": 0.35, "Contrast": 12, "Highlights": -40, "...": "..." },
"full_copy": null,
"software": null,
"engine_version": 5
},
"error": null
}Authentication
Send your key as Authorization: Bearer <key> (or X-API-Key: <key>). Keys start with fp_live_, are shown once when you create them, and can be revoked at any time in your account. An account can have 5 active keys at a time; all of them spend the account's credits.
Call the API from your server. It sends no CORS headers, so other websites cannot call it from a browser, and a key never needs to ship in a web page or an app.
Operations and credits
| Operation | What runs | Send | Credits |
|---|---|---|---|
exact | Reads the Lightroom settings embedded in the photo | image, or xmp (the settings packet or a .xmp sidecar) | 0.1 per read, settings found or not |
ai | An AI-extracted preset from one photo | image, optional creativity (0 to 100, default 50) | 1 |
compare | Style match: a preset that moves the target toward the reference | reference and target, optional creativity | 1 |
auto | Exact when the photo carries settings, else AI | image, optional xmp and creativity | 0.1 when settings are found, else 1 |
A failed request (the model was busy, the run timed out, no preset came back) costs nothing: its credit, or its exact read, is given back before anyone can read it as failed. A request we refuse (a wrong key, a missing, unsupported or oversized file, an image the AI cannot decode, no credits left) costs nothing. An exact read is charged even when the file turns out to hold no settings. Every request is a fresh run: the same photo sent twice without an Idempotency-Key runs and is billed twice.
An exact read returns the photo's own settings. When we cannot copy them as they are, we rebuild the file from the settings we could read and say so with full_copy: false; a few settings may then be missing. An AI result is an estimate of the edit, made by looking at the photo.
Pricing
The API comes with Premium, $4.99 a month or $39.99 a year, and spends the same plan and bonus credits as findpreset.com: Premium gives 20 a month on Monthly, or 240 a year on Annual, and bonus credits count too. It never spends a credit bought on the site to unlock a single preset. There is no separate API plan. Need a bigger quota than Premium gives? Write to support@findpreset.com.
Exact reads come 10 to a credit: the first read spends 1 credit and opens 10 reads, and the next 9 use them. A read that fails goes back to the reads you have. When the credits run out, requests answer 402 insufficient_credits; there is no overage billing. A request from an account without Premium gets 403 plan_required and is never charged; results it made before stay readable until they are erased. Premium is in your account.
Endpoints
POST /api/v1/extractions takes multipart/form-data with the fields above. An exact read answers at once with 200. An AI request answers 202 with the extraction's id, a Location header and status: "processing"; it usually finishes in under a minute. Add ?wait=true to keep the connection open until it does (up to about four and a half minutes, then you get the 202 to poll). With wait=true, also send an Idempotency-Key, so a dropped connection can be retried without running twice. Your account runs up to 2 AI requests at a time.
GET /api/v1/extractions/{id} returns the extraction: 202 while it is processing, 200 once it is succeeded, not_found (an exact read found no settings) or failed. Poll every few seconds; the Retry-After header says how long to wait.
curl https://findpreset.com/api/v1/extractions/REQUEST_ID \ -H "Authorization: Bearer $FINDPRESET_API_KEY"
GET /api/v1/extractions/{id}/preset.xmp downloads the preset as a .xmp file, the same file as result.preset_xmp; add ?variant=masks for the version with local adjustments. It needs the API key of the account that made the request, so a link that leaks downloads nothing. While the request is processing it answers 409 result_not_ready.
curl -OJ https://findpreset.com/api/v1/extractions/REQUEST_ID/preset.xmp \ -H "Authorization: Bearer $FINDPRESET_API_KEY"
GET /api/v1/balance returns whether the account has API access, its credits, the exact reads it has left, and the price of each operation. That answer, every answer to POST /api/v1/extractions that creates or replays an extraction, and the insufficient_credits refusals carry X-Credits-Remaining and X-Exact-Reads-Left. Polls and downloads do not.
The extraction
| Field | Meaning |
|---|---|
operation | What you asked for. |
method | What ran: exact, ai or compare (auto resolves to exact or ai). |
status | processing, succeeded, not_found or failed. |
credits | Credits it costs: its price while processing (1 for AI, 0.1 for an exact read), what it cost once done (0 when nothing was charged). |
result.preset_xmp | The Lightroom preset file. |
result.preset_xmp_with_masks | The same preset with its local adjustments (masks), when it has any. |
result.preset_url | Where preset_xmp downloads as a file, with your API key. |
result.preset_with_masks_url | Where preset_xmp_with_masks downloads; null when the preset has no masks. |
result.settings | The preset's global settings, read back from preset_xmp (for example Exposure, Contrast, HSL_Hue_Red). New keys may be added. |
result.full_copy | Exact reads: true when the file copies the photo’s own settings, false when it was rebuilt from what we could read. Null for AI. |
result.software | Exact reads: the editing software named in the photo. |
result.engine_version | AI results: the version of the extraction engine that made the preset. |
result_expires_at | When the result is erased (7 days after it finished). |
error | Why a request ended without a preset: code and message. |
| Error code on an extraction | Meaning |
|---|---|
no_settings | An exact read found no Lightroom settings in the photo (status not_found). The read is charged (a tenth of a credit). |
overloaded | The AI was busy. The credit was given back. |
ai_error | The AI could not extract a preset from this photo. The credit was given back. |
parse_error | The AI answer could not be read as a preset. The credit was given back. |
timeout | The run did not finish in time. The credit was given back. |
internal_error | Our fault. The credit (or the exact read) was given back. |
Errors
A refused request answers { "error": { "code", "message" } }. Codes are stable; messages are for people and may change. 409 result_not_ready, 429 and 503 answers carry Retry-After when waiting helps.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key | No API key was sent. |
| 401 | invalid_api_key | The key is wrong, revoked, or from another environment. |
| 429 | rate_limited | Too many requests with a wrong key from your address. Wait a minute. |
| 400 | bad_request | The body is not multipart/form-data, or a field has the wrong type. |
| 400 | missing_operation | No "operation" field. |
| 400 | unsupported_operation | The operation is not exact, ai, compare or auto. |
| 400 | missing_file | The operation needs an image the request does not have. |
| 400 | bad_creativity | "creativity" is not a whole number from 0 to 100. |
| 400 | bad_idempotency_key | The Idempotency-Key header is empty, too long or has spaces. |
| 400 | unreadable_image | The image could not be decoded. |
| 402 | insufficient_credits | Not enough credits for this request. Nothing was charged. |
| 403 | plan_required | The API comes with Premium, and this account does not have it. Nothing was charged. |
| 404 | not_found | No such request for this account, or no such endpoint. |
| 404 | no_preset | The request finished without a preset file (it failed, or the photo had no settings to read), or has no version with local adjustments. |
| 405 | method_not_allowed | The endpoint does not take this HTTP method. |
| 409 | idempotency_key_reused | This Idempotency-Key was used for a different request. |
| 409 | result_not_ready | The request is still processing, so its preset file is not ready yet. Retry after the Retry-After seconds. |
| 410 | result_expired | The request finished more than 7 days ago; its result was erased. |
| 413 | file_too_large | An image is over 4 MB, or "xmp" is over 1 MB. |
| 413 | request_too_large | The whole request is over 4.5 MB. |
| 415 | unsupported_file | A file is not an image we accept, or is a HEIC or raw file sent for AI. |
| 429 | concurrency_limit | Your account already has its maximum number of AI requests running. |
| 429 | failure_limit | Too many AI requests on this account failed in the last 24 hours. Nothing was charged; exact reads still work, and AI works again once those failures are more than 24 hours old. |
| 500 | internal_error | Our fault. A request that was not opened costs nothing; to check one that was, retry with the same Idempotency-Key, which never runs or bills twice. |
| 503 | capacity | The AI is at capacity across all customers. Retry after the Retry-After seconds. |
Limits, retries and data
Each image can be up to 4 MB and a whole request up to 4.5 MB. The AI reads photos at 1568 pixels on the long edge, so a copy that size is all an AI request needs: send it as JPEG (quality 90 or more), PNG, WebP or TIFF, in sRGB or with its color profile kept. Exact reads also take HEIC and DNG, and for an original over the limit you can send its settings as xmp instead (resizing a photo usually drops its settings).
A failed AI request costs you nothing but usually costs us the model call, so when 10 or more AI requests on an account fail because of the photo (ai_error or parse_error) in 24 hours and they outnumber the ones that succeeded, AI requests answer 429 failure_limit until those failures are more than 24 hours old. Exact reads still work, and a busy model, a timeout or an error on our side never counts.
Retries are safe with an Idempotency-Key header (1 to 255 printable characters): for 90 days, the same key returns the first request as it stands and never runs or bills again, even while it is still processing. A replay is a request like any other, so it needs Premium; GET /api/v1/extractions/{id} reads a request any time until its result is erased. The same key with a different photo, operation or creativity answers 409 idempotency_key_reused. A failed request stays failed for its key; use a new key to try again.
We do not keep your photos: they are read for the request and dropped when it ends. We keep each request's record (operation, status, credits, times and the file name) for 90 days and its result for 7 days; the credits the API spends show in your credit history, as on the site. Details are in the privacy policy and the API section of the terms.