Skip to main content

Files API: list and download the files shoppers upload in your quiz

For developers: count, list, search and download the files shoppers upload in your quiz, using the API key on the Integrations page.

Written by Traian

A quiz can ask shoppers to upload files, for example a photo of their skin, a prescription or a picture of a room (see Customer file upload in Lantern quizzes). In the Lantern admin those files appear under Reporting > Uploaded Files, where you can view and download them one at a time. The Files API gives your own software the same access, so a developer can pull the files into another system: a patient record, a CRM, a design tool, an order management tool.

With the API you can:

• count the files your shoppers uploaded

• list them page by page, newest first

• narrow the list to one quiz, one quiz session, one shopper's email address, part of a file name, or a range of upload dates

• download any file

Files from every quiz session are included, whether the shopper finished the quiz or left before the results. The quiz completion webhook, by comparison, only fires when a shopper reaches the results page, so it never carries files from unfinished sessions. If you need every upload, use this API.

This article is written for developers. If you only want to look at the files, Reporting > Uploaded Files is the place.

Before you start

Base URL: every request goes to https://api.productfinder.app. Only HTTPS is accepted.

Format: requests are plain GET requests with parameters in the URL. Answers are JSON. Dates and times are in UTC, written in ISO 8601 (for example 2026-10-23T01:15:28.613Z).

Your API key: in the Lantern admin open Integrations. The card at the top of the left column reads Your API key is: click Reveal, then Copy. It is the same key you would use to connect Zapier. Keep it secret: anyone who has it can read every file your shoppers uploaded and every quiz result sent to Zapier.

Your store domain: the myshopify.com address of your store, for example your-store.myshopify.com. You can see it in the address bar of your Shopify admin. It stays the same when you connect a custom domain.

Authentication

Send two headers with every request:

Header

Value

X-STORE

Your store domain, for example your-store.myshopify.com

X-API-KEY

The API key from the Integrations page

A request with a missing or wrong header gets the answer 401 Unauthorized. The key only opens the files of the store it belongs to: a request that names another store's domain with your key is refused.

A first request to check your setup:

curl "https://api.productfinder.app/v1/files?limit=1" \
-H "X-STORE: your-store.myshopify.com" \
-H "X-API-KEY: your-api-key"

If the headers are right you get a JSON answer with your newest file in items and the number of files you have in total. If you have no files yet, items is empty and total is 0.

Endpoints

Request

What it returns

GET /v1/files

A page of files, newest first, with filters and the total count

GET /v1/files/{id}

One file's details

GET /v1/files/{id}/download

A redirect to a temporary link to the file itself

List files

GET https://api.productfinder.app/v1/files

Returns the newest files first, 25 per page unless you set limit. All parameters are optional and can be combined; a file has to match every parameter you send.

Parameter

Type

Meaning

session_id

UUID

Only files uploaded during one quiz session. It is the same session id your quiz completion webhook sends and Reporting shows.

quiz_id

text

Only files uploaded through one quiz. This is the quiz's URL slug, the last part of the quiz address, for example skin-quiz. It is also the quiz_id in the completion webhook.

email

text

Only files from sessions where the shopper entered this email address. Capitalization does not matter.

file_name

text

Only files whose name contains this text, for example photo matches my-photo.jpg. Capitalization does not matter.

uploaded_after

date

Only files uploaded at or after this moment. ISO 8601 (2026-10-01T00:00:00Z) or a Unix timestamp in milliseconds.

uploaded_before

date

Only files uploaded before this moment. Same formats.

limit

number

Files per page, 1 to 100. Default 25.

offset

number

Files to skip before the page starts. Default 0. For the second page of 25, send 25.

Example: the files uploaded through the quiz skin-quiz in October 2026, 50 per page.

curl "https://api.productfinder.app/v1/files?quiz_id=skin-quiz&uploaded_after=2026-10-01T00:00:00Z&uploaded_before=2026-11-01T00:00:00Z&limit=50" \
-H "X-STORE: your-store.myshopify.com" \
-H "X-API-KEY: your-api-key"

The answer:

{
"total": 2,
"limit": 50,
"offset": 0,
"items": [
{
"id": "6f2c1c1e-3d7a-4b8e-9f10-2a6c8d4e5b71",
"file_name": "left-cheek.jpg",
"content_type": "image/jpeg",
"size": 281380,
"uploaded_at": "2026-10-23T01:15:28.613Z",
"download_url": "https://api.productfinder.app/v1/files/6f2c1c1e-3d7a-4b8e-9f10-2a6c8d4e5b71/download",
"session_id": "4b3f9a1e-7f5e-4c1a-9c2d-2b8e6f1a0d3c",
"quiz_name": "Skin Quiz",
"quiz_id": "skin-quiz",
"email": "[email protected]",
"session_started_at": "2026-10-23T01:14:02.000Z",
"session_completed_at": "2026-10-23T01:16:25.431Z",
"test_session": false
},
{
"id": "0a9d7c33-5e21-4f6b-8c44-1d2e3f4a5b6c",
"file_name": "forehead.png",
"content_type": "image/png",
"size": 2506411,
"uploaded_at": "2026-10-21T09:02:11.004Z",
"download_url": "https://api.productfinder.app/v1/files/0a9d7c33-5e21-4f6b-8c44-1d2e3f4a5b6c/download",
"session_id": "a9ca397d-fefb-498a-bf94-2b9534d8453c",
"quiz_name": "Skin Quiz",
"quiz_id": "skin-quiz",
"email": null,
"session_started_at": "2026-10-21T09:00:40.112Z",
"session_completed_at": null,
"test_session": false
}
]
}

Fields in the answer

Field

Type

Meaning

total

number

How many files match your filters across all pages. With no filters, how many files you have in all.

limit, offset

number

The page size and start you asked for (or the defaults).

items

list

The files on this page, newest first. Each item has the fields below.

id

UUID

The file's id. Use it for the single-file and download requests.

file_name

text

The name the file had on the shopper's device.

content_type

text

The file's media type, for example image/jpeg or application/pdf. Can be null when the browser did not report one.

size

number

Size in bytes.

uploaded_at

date

When the upload finished.

download_url

URL

The permanent download address for this file. See Download a file.

session_id

UUID

The quiz session the file belongs to.

quiz_name, quiz_id

text

The quiz's name and URL slug at the time of the request.

email

text

The email address the shopper entered in the quiz, or null if they did not enter one.

session_started_at

date

When the shopper started the quiz.

session_completed_at

date

When the shopper reached the results, or null if they left the quiz before the results page.

test_session

true / false

True for sessions started from the quiz preview in the editor, false for real shoppers.

Reading every page

Start with offset=0, then add the number of items you received to the offset and ask again, until the offset reaches total. Files are sorted newest first, so a file uploaded while you page through moves everything down by one. If you sync regularly, filter with uploaded_after set to the time of your previous sync instead of reading the whole list every time.

One file

GET https://api.productfinder.app/v1/files/{id}

Returns one file with the same fields as an entry in the list. Use it to refresh a file's details when you already know its id.

curl "https://api.productfinder.app/v1/files/6f2c1c1e-3d7a-4b8e-9f10-2a6c8d4e5b71" \
-H "X-STORE: your-store.myshopify.com" \
-H "X-API-KEY: your-api-key"

A file id that does not exist in your store gets the answer 404 Not Found, including ids that belong to another store.

Download a file

GET https://api.productfinder.app/v1/files/{id}/download

This is the download_url from the list. Send the same two headers. The answer is a 302 redirect whose Location header is a temporary link to the file itself, on Lantern's storage. That temporary link needs no headers and works for 15 minutes, then expires. Its Content-Disposition header carries the original file name.

There are two ways to use it:

1. Let your HTTP client follow the redirect. Most clients do this by default; with curl add -L. The file is downloaded in one step.

curl -L "https://api.productfinder.app/v1/files/6f2c1c1e-3d7a-4b8e-9f10-2a6c8d4e5b71/download" \
-H "X-STORE: your-store.myshopify.com" \
-H "X-API-KEY: your-api-key" \
-o left-cheek.jpg

2. Read the Location header yourself and fetch that link without any headers. This keeps your API key out of the request to the storage server, and it lets you hand the temporary link to a browser or another service that needs to read the file within the next 15 minutes.

curl -s -o /dev/null -w "%{redirect_url}" \
"https://api.productfinder.app/v1/files/6f2c1c1e-3d7a-4b8e-9f10-2a6c8d4e5b71/download" \
-H "X-STORE: your-store.myshopify.com" \
-H "X-API-KEY: your-api-key"

Store the download_url in your system, never the temporary link. The download_url does not expire and gives you a fresh temporary link every time you call it.

Errors

Errors come back as JSON with the HTTP status and a message that says what was wrong:

{ "statusCode": 400, "message": "session_id must be a UUID", "error": "Bad Request" }

Status

Meaning

400 Bad Request

A parameter is not valid: a session_id that is not a UUID, a date that cannot be read, a limit of 0, a negative offset. The message names the parameter.

401 Unauthorized

The X-STORE or X-API-KEY header is missing or wrong, or the key does not belong to that store.

404 Not Found

No file with that id exists in your store.

Code examples

Both examples list every file uploaded since a given moment and download each one into a folder, with the file's id in front of the name so two shoppers uploading photo.jpg do not overwrite each other.

JavaScript (Node.js 18 or newer)

const BASE = "https://api.productfinder.app";
const headers = {
"X-STORE": "your-store.myshopify.com",
"X-API-KEY": "your-api-key",
};

async function listFiles(since) {
const files = [];
let offset = 0;
while (true) {
const url = `${BASE}/v1/files?limit=100&offset=${offset}&uploaded_after=${encodeURIComponent(since)}`;
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
files.push(...page.items);
offset += page.items.length;
if (offset >= page.total || page.items.length === 0) break;
}
return files;
}

async function download(file, folder) {
const res = await fetch(file.download_url, { headers }); // follows the redirect
if (!res.ok) throw new Error(`${res.status} downloading ${file.id}`);
const bytes = Buffer.from(await res.arrayBuffer());
await require("fs/promises").writeFile(`${folder}/${file.id}-${file.file_name}`, bytes);
}

(async () => {
const files = await listFiles("2026-10-01T00:00:00Z");
console.log(`${files.length} files`);
for (const file of files) await download(file, "./uploads");
})();

Python 3 (requests)

import requests

BASE = "https://api.productfinder.app"
HEADERS = {
"X-STORE": "your-store.myshopify.com",
"X-API-KEY": "your-api-key",
}

def list_files(since):
files, offset = [], 0
while True:
res = requests.get(
f"{BASE}/v1/files",
headers=HEADERS,
params={"limit": 100, "offset": offset, "uploaded_after": since},
)
res.raise_for_status()
page = res.json()
files.extend(page["items"])
offset += len(page["items"])
if offset >= page["total"] or not page["items"]:
return files

def download(file, folder):
res = requests.get(file["download_url"], headers=HEADERS) # follows the redirect
res.raise_for_status()
with open(f"{folder}/{file['id']}-{file['file_name']}", "wb") as out:
out.write(res.content)

files = list_files("2026-10-01T00:00:00Z")
print(len(files), "files")
for file in files:
download(file, "./uploads")

Good to know

• Which plans: the file upload block itself is available on the Grow and Enterprise plans. The API reads whatever files your store has, on any plan, including files uploaded before you downgraded.

• File types and size: the upload block accepts any file type up to 5 GB. The API returns them unchanged, exactly as uploaded.

• Deletion: all of a store's files are deleted 48 hours after Lantern is uninstalled, together with the rest of the store's quiz data, so download what you need while the app is installed. To delete one shopper's files earlier, write to us through the chat in the Lantern admin.

• Test sessions: files uploaded while previewing the quiz in the editor are listed with test_session true. Skip them in your sync if you only want real shoppers.

• Be reasonable with requests: a sync every few minutes with uploaded_after is fine. Avoid re-reading your entire file list in a tight loop.

• Need something the API does not do? Write to us through the chat in the Lantern admin and tell us what you are building.

Did this answer your question?