Group Shoot
Overview
Public REST API for integrating HR, CRM, or ERP systems with exportlab.io group shoot galleries, plus public endpoints for custom check-in flows.
List participants and retrieve their assigned photos, all scoped to your tenant.
/group-shoot/checkin/{token}Returns public metadata needed for a custom check-in page.
/group-shootCreates a person from a public check-in form.
/group-shoot/checkin/{token}/personsReturns all participants registered for a group shoot gallery.
/group-shoot/checkin/{token}/personsCreate multiple persons in one request.
/group-shoot/checkin/{token}/persons/{personId}/photosReturns all photos assigned to a specific person with signed CDN URLs.
/group-shoot/checkin/{token}/daysReturns all shooting days configured for a gallery.
/group-shoot/checkin/{token}/form-fieldsReturns the configured check-in form fields for a gallery.
/group-shoot/checkin/{token}/statsReturns aggregated stats for a group shoot gallery.
Base URL
https://api.exportlab.io/v1Authentication
Every request requires two headers.
Get your API key in the exportlab.io admin under Settings → Integration → API Keys.
Go to Settings → Integration, scroll to the API Keys section, click New key, give it a name (example: HR Integration), and copy the key immediately. It is only shown once.
The API is designed for server-to-server use. Never expose your API key in a browser or client-side code.
Public check-in endpoints (GET /group-shoot/checkin/{token} and POST /group-shoot) do not require an API key, but they still require a valid tenant slug.
| Header | Description |
|---|---|
| Authorization: Bearer | Your API key. Also accepted as X-Api-Key: |
| X-Tenant-Slug: | Your tenant identifier (example: acme) |
Response format
All responses are JSON.
The X-Request-Id response header contains the same request ID. Include it in support requests.
{
"ok": true,
"requestId": "6322678e-7898-43eb-baeb-5de42b07ca7f"
}Rate limiting
The API enforces a limit of 60 requests per minute per API key. If you exceed the limit you receive a 429 response.
Wait until X-RateLimit-Reset before retrying. For bulk imports, add a small delay between requests to stay within the limit.
{
"ok": false,
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Max 60 requests per minute. Retry after 42s.",
"requestId": "..."
}| Header | Description |
|---|---|
| X-RateLimit-Limit | Max requests allowed per window (60) |
| X-RateLimit-Remaining | Requests remaining in the current window |
| X-RateLimit-Reset | Unix timestamp (seconds) when the window resets |
Get check-in info (public)
GET /group-shoot/checkin/{token}
Returns basic metadata required to render a custom check-in page.
This endpoint is public and does not require an API key.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}" \
-H "X-Tenant-Slug: YOUR_TENANT"{
"ok": true,
"galleryName": "Team Headshots – Spring 2026",
"selfieEnabled": true,
"tenantSlug": "acme",
"token": "iURWUkHo",
"requestId": "a41c49f0-..."
}| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| Response field | Type | Description |
|---|---|---|
| galleryName | string | Gallery display name |
| selfieEnabled | boolean | Whether the check-in flow accepts a selfie upload |
| tenantSlug | string | Tenant slug resolved for this request |
| token | string | Gallery token |
Submit check-in (public)
POST /group-shoot
Creates a person from a public check-in form.
This endpoint is public and does not require an API key.
{
"action": "submitCheckin",
"token": "iURWUkHo",
"name": "Max Mustermann",
"email": "max@example.com",
"employeeId": "EMP-001",
"selfieBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
}curl -X POST "https://api.exportlab.io/v1/group-shoot" \
-H "Content-Type: application/json" \
-H "X-Tenant-Slug: YOUR_TENANT" \
-d '{
"action": "submitCheckin",
"token": "iURWUkHo",
"name": "Max Mustermann",
"email": "max@example.com",
"employeeId": "EMP-001"
}'{
"ok": true,
"personId": "mZTZBszq",
"code": "LWR7",
"name": "Max Mustermann",
"requestId": "b4ad3a02-..."
}| Field | Type | Required | Description |
|---|---|---|---|
| action | string | Yes | Must be submitCheckin |
| token | string | Yes | Gallery token |
| name | string | Yes | Full name (1–200 chars) |
| string | No | Email address (max 200 chars) | |
| employeeId | string | No | HR system employee ID (max 100 chars) |
| selfieBase64 | string | No | Base64-encoded JPEG. Accepts full data URI or raw base64 |
List persons
GET /group-shoot/checkin/{token}/persons
Returns all participants registered for a group shoot gallery.
When nextCursor is null, you have reached the last page.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/persons" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT"{
"ok": true,
"galleryToken": "iURWUkHo",
"requestId": "6322678e-7898-43eb-baeb-5de42b07ca7f",
"total": 250,
"limit": 100,
"nextCursor": "MTAw",
"persons": [
{
"personId": "mZTZBszq",
"name": "Max Mustermann",
"email": "max@example.com",
"employeeId": "EMP-001",
"shootingDayId": "day-abc123",
"selectedPackageId": "pkg-xyz456",
"code": "LWR7",
"photoCount": 3,
"checkedInAt": "2026-03-06T13:27:11.409Z",
"createdAt": "2026-03-06T13:27:11.409Z",
"customFields": {
"custom-a1b2c3d4-...": "L"
}
}
]
}| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token (visible in the admin URL) |
| Query parameter | Type | Required | Description |
|---|---|---|---|
| employeeId | string | No | Filter to return only the person with this employee ID |
| shootingDayId | string | No | Filter to return only persons assigned to this shooting day ID |
| limit | number | No | Page size, 1-100. Default: 100 |
| cursor | string | No | Pagination cursor from the previous response's nextCursor |
| Person field | Type | Description |
|---|---|---|
| personId | string | Unique person ID within this gallery |
| name | string | Full name |
| string | null | Email address if provided at check-in | |
| employeeId | string | null | HR system employee ID if provided |
| shootingDayId | string | null | ID of the assigned shooting day. null if no day is assigned |
| selectedPackageId | string | null | ID of the selected package, if any |
| code | string | 4-character personal access code (example: LWR7) |
| photoCount | number | Number of photos assigned to this person |
| checkedInAt | ISO 8601 | null | When the person checked in. null if added manually by admin |
| createdAt | ISO 8601 | When the record was created |
| customFields | object | — | Key-value map of custom form field responses (only present when configured) |
Import persons (bulk)
POST /group-shoot/checkin/{token}/persons
Creates multiple persons for a group shoot gallery in a single request. Designed for HR/CRM systems that need to pre-populate participants before the photo session.
Each person gets a unique 4-character access code automatically. Up to 500 persons per request.
{
"persons": [
{ "name": "Max Mustermann", "email": "max@example.com", "employeeId": "EMP-001", "shootingDayId": "abc12345" },
{ "name": "Erika Musterfrau", "employeeId": "EMP-002" },
{ "name": "John Doe" }
]
}curl -X POST "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/persons" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT" \
-H "Content-Type: application/json" \
-d '{
"persons": [
{ "name": "Max Mustermann", "email": "max@example.com", "employeeId": "EMP-001" },
{ "name": "Erika Musterfrau", "employeeId": "EMP-002" }
]
}'{
"ok": true,
"requestId": "c3d4e5f6-...",
"galleryToken": "iURWUkHo",
"imported": 2,
"skipped": 0,
"persons": [
{ "personId": "mZTZBszq", "name": "Max Mustermann", "email": "max@example.com", "employeeId": "EMP-001", "code": "LWR7" },
{ "personId": "nXaYcTdr", "name": "Erika Musterfrau", "email": null, "employeeId": "EMP-002", "code": "KP3M" }
]
}{
"ok": true,
"imported": 1,
"skipped": 1,
"persons": [...],
"skippedDetails": [
{ "index": 1, "reason": "name_required" }
]
}Idempotency
The API does not deduplicate by employeeId or name. Calling the endpoint twice with the same data creates duplicate persons. Check via GET /persons?employeeId= before importing if you need idempotency.
| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| Field | Type | Required | Description |
|---|---|---|---|
| persons | array | Yes | List of persons to import (max 500) |
| persons[].name | string | Yes | Full name (1–200 chars) |
| persons[].email | string | No | Email address (max 200 chars) |
| persons[].employeeId | string | No | HR system employee ID (max 100 chars) |
| persons[].shootingDayId | string | No | Shooting day ID from GET /days. Invalid IDs are silently ignored |
| Response field | Type | Description |
|---|---|---|
| imported | number | Number of persons successfully created |
| skipped | number | Number of rows skipped due to validation errors |
| persons | array | Created persons with their generated code and personId |
| skippedDetails | array | Only present when skipped > 0 — lists index and reason for each skipped row |
| Skip reason | Description |
|---|---|
| name_required | name field is missing or empty |
| code_generation_failed | Could not generate a unique code after 10 attempts |
| write_failed | Database write failed for this row |
Get photos for a person
GET /group-shoot/checkin/{token}/persons/{personId}/photos
Returns all photos assigned to a specific person, including signed CDN URLs for full resolution, thumbnail, and mini variants. URLs are valid for 24 hours.
If no photos are assigned yet, photos is an empty array.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/persons/{PERSON_ID}/photos" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT"{
"ok": true,
"requestId": "3fe4f8f8-b006-4d99-ab78-cf1b95edc102",
"person": {
"personId": "mZTZBszq",
"name": "Max Mustermann",
"employeeId": "EMP-001",
"favoriteCount": 1
},
"photos": [
{
"filename": "5fdf5fdf-bb19-4aa1-8a0c-c6d189b66351-DSC01909.jpg",
"isFavorite": true,
"url": "https://cdn.exportlab.io/tenants/acme/library/iURWUkHo/...?Expires=...&Signature=...&Key-Pair-Id=...",
"thumbUrl": "https://cdn.exportlab.io/tenants/acme/library/iURWUkHo/....thumb.jpg?...",
"miniUrl": "https://cdn.exportlab.io/tenants/acme/library/iURWUkHo/....mini.jpg?...",
"width": 4000,
"height": 6000,
"size": 267297,
"takenAt": "2026-03-06T12:18:27.157Z",
"createdAt": "2026-03-06T12:18:27.049Z"
}
]
}Favorites
Favorites must be enabled per gallery in the admin settings. If disabled, isFavorite is always false and ?filter=favorites returns an empty array.
| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| personId | string | The person ID from the list endpoint |
| Query parameter | Type | Required | Description |
|---|---|---|---|
| filter | string | No | Use favorites to return only photos marked as favorite |
| Photo field | Type | Description |
|---|---|---|
| filename | string | Original filename |
| isFavorite | boolean | Whether this photo is marked as favorite |
| url | string | Signed URL, full resolution original (valid 24h) |
| thumbUrl | string | Signed URL, thumbnail variant, ~512px wide (valid 24h) |
| miniUrl | string | Signed URL, mini variant, ~128px wide (valid 24h) |
| width | number | null | Width in pixels |
| height | number | null | Height in pixels |
| size | number | null | File size in bytes |
| takenAt | ISO 8601 | null | EXIF capture time if available |
| createdAt | ISO 8601 | Upload timestamp |
| Person field | Type | Description |
|---|---|---|
| personId | string | Unique person ID within this gallery |
| name | string | Full name |
| employeeId | string | null | HR system employee ID if provided |
| favoriteCount | number | Number of photos marked as favorite |
List shooting days
GET /group-shoot/checkin/{token}/days
Returns all shooting days configured for the gallery. Shooting days are optional, if none are configured, shootingDays is an empty array.
This endpoint is public and does not require an API key.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/days"{
"ok": true,
"galleryToken": "iURWUkHo",
"shootingDays": [
{
"id": "abc12345",
"date": "2026-04-10",
"label": "Team A – Morning",
"startTime": "09:00",
"endTime": "12:00",
"maxPersons": 50,
"notes": "Please bring your ID badge",
"spotsRemaining": 23,
"createdAt": "2026-03-01T14:22:00.000Z"
}
]
}| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| Response field | Type | Description |
|---|---|---|
| shootingDays | array | All shooting days for this gallery, sorted by date ascending |
| shootingDays[].id | string | Shooting day ID (used as shootingDayId on persons) |
| shootingDays[].date | string | Date in YYYY-MM-DD format |
| shootingDays[].label | string | null | Optional display label |
| shootingDays[].startTime | string | null | Start time in HH:MM format |
| shootingDays[].endTime | string | null | End time in HH:MM format |
| shootingDays[].maxPersons | number | null | Capacity limit, or null for unlimited |
| shootingDays[].notes | string | null | Optional admin notes visible in the check-in flow |
| shootingDays[].spotsRemaining | number | null | Spots still available (null if unlimited) |
| shootingDays[].createdAt | string | ISO 8601 timestamp when the day was created |
Get check-in form fields
GET /group-shoot/checkin/{token}/form-fields
Returns the configured check-in form fields for a gallery, useful for building your own check-in UI or for mapping customFields values from the persons endpoint to human-readable labels.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/form-fields" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT"{
"ok": true,
"galleryToken": "iURWUkHo",
"formFields": [
{
"key": "employeeId",
"label": "Employee ID",
"type": "text",
"required": false
},
{
"key": "custom-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"label": "T-shirt size",
"type": "select",
"required": true,
"options": [
{ "value": "S", "label": "S" },
{ "value": "M", "label": "M" },
{ "value": "L", "label": "L" },
{ "value": "XL", "label": "XL" }
]
}
]
}Only enabled fields are returned. Name and email are always required and are not included in this list.
Use the key to map customFields values from GET /persons. For example customFields["custom-a1b2c3d4-..."] corresponds to the field with that key.
| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| Field | Type | Description |
|---|---|---|
| key | string | Field identifier. Built-in: employeeId, phone, department, notes. Custom: custom-{uuid} |
| label | string | Display label as configured by the admin |
| type | string | text, textarea, number, or select |
| required | boolean | Whether the field must be filled in |
| options | array | Only present when type is select. Each entry has value and label |
Get gallery stats
GET /group-shoot/checkin/{token}/stats
Returns aggregated statistics for a group shoot gallery, useful for dashboards, progress tracking, or reporting.
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/stats" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT"{
"ok": true,
"requestId": "a1b2c3d4-...",
"galleryToken": "iURWUkHo",
"totalPersons": 250,
"checkedInCount": 187,
"checkInRate": 74.8,
"personsWithPhotos": 156,
"personsWithoutPhotos": 31,
"uniquePhotosAssigned": 468,
"totalAssignments": 512,
"personsWithFavorites": 89,
"totalFavorites": 102,
"byShootingDay": [
{
"dayId": "abc12345",
"date": "2026-04-10",
"label": "Team A – Morning",
"totalPersons": 45,
"checkedInCount": 30,
"personsWithPhotos": 28
}
]
}| Path parameter | Type | Description |
|---|---|---|
| token | string | The gallery token |
| Response field | Type | Description |
|---|---|---|
| totalPersons | number | Total number of persons registered for this gallery |
| checkedInCount | number | Persons who checked in via the self-service form |
| checkInRate | number | Percentage of persons checked in (0–100, one decimal) |
| personsWithPhotos | number | Persons with at least one assigned photo |
| personsWithoutPhotos | number | Persons with no photos assigned yet |
| uniquePhotosAssigned | number | Number of unique photos assigned across all persons |
| totalAssignments | number | Total photo-person assignments |
| personsWithFavorites | number | Persons who have at least one favorite photo set |
| totalFavorites | number | Total number of favorite photo marks across all persons |
| byShootingDay | array | Per-day breakdown. Empty array if no shooting days are configured |
| byShootingDay[].dayId | string | Shooting day ID |
| byShootingDay[].date | string | Date in YYYY-MM-DD format |
| byShootingDay[].label | string | Day label (empty string if not set) |
| byShootingDay[].totalPersons | number | Persons assigned to this day |
| byShootingDay[].checkedInCount | number | Persons for this day who checked in |
| byShootingDay[].personsWithPhotos | number | Persons for this day with at least one photo assigned |
URL aliases
Both URL patterns are equivalent and fully supported.
| Method | Pattern | Description |
|---|---|---|
| GET | /group-shoot/checkin/{token}/form-fields | Primary |
| GET | /groupshoot/{token}/form-fields | Short alias |
| GET | /group-shoot/checkin/{token}/days | Primary |
| GET | /groupshoot/{token}/days | Short alias |
| GET | /group-shoot/checkin/{token}/stats | Primary |
| GET | /groupshoot/{token}/stats | Short alias |
| GET | /group-shoot/checkin/{token}/persons | Primary |
| GET | /groupshoot/{token}/persons | Short alias |
| POST | /group-shoot/checkin/{token}/persons | Primary (import) |
| POST | /groupshoot/{token}/persons | Short alias (import) |
| GET | /group-shoot/checkin/{token}/persons/{personId}/photos | Primary |
| GET | /groupshoot/{token}/persons/{personId}/photos | Short alias |
Error codes
| HTTP | error | Description |
|---|---|---|
| 400 | missing_tenant_slug | X-Tenant-Slug header is missing |
| 400 | invalid_tenant | Tenant slug could not be resolved |
| 400 | not_a_groupshoot_gallery | The gallery token exists but is not a group shoot |
| 400 | token_required | Gallery token is missing |
| 400 | name_required | Name is missing or empty |
| 400 | invalid_json | Request body is not valid JSON |
| 401 | api_key_required | No API key provided |
| 401 | invalid_api_key | API key is wrong, revoked, or disabled |
| 404 | gallery_not_found | Gallery token does not exist |
| 404 | person_not_found | Person ID does not exist in this gallery |
| 405 | method_not_allowed | Only GET is supported |
| 409 | shooting_day_full | The selected shooting day has reached its maxPersons capacity |
| 429 | rate_limit_exceeded | Too many requests. See X-RateLimit-Reset header |
| 500 | internal_error | Server error, contact support with the requestId |
Backend integration example (Node.js)
Minimal proxy pattern, useful if you want to add your own auth layer or combine the data with other sources.
const API_BASE = "https://api.exportlab.io";\nconst API_KEY = process.env.GROUPSHOOT_API_KEY; // starts with sk_\nconst TENANT_SLUG = process.env.GROUPSHOOT_TENANT_SLUG;\n\nasync function listPersons(galleryToken, employeeId) {\n const url = new URL(`${API_BASE}/group-shoot/checkin/${galleryToken}/persons`);\n if (employeeId) url.searchParams.set("employeeId", employeeId);\n\n const res = await fetch(url, {\n headers: {\n "X-Api-Key": API_KEY,\n "X-Tenant-Slug": TENANT_SLUG,\n },\n });\n return res.json();\n}\n\nasync function getPersonPhotos(galleryToken, personId) {\n const res = await fetch(\n `${API_BASE}/group-shoot/checkin/${galleryToken}/persons/${personId}/photos`,\n {\n headers: {\n "X-Api-Key": API_KEY,\n "X-Tenant-Slug": TENANT_SLUG,\n },\n }\n );\n return res.json();\n}\n\nasync function importPersons(galleryToken, persons) {\n const res = await fetch(\n `${API_BASE}/group-shoot/checkin/${galleryToken}/persons`,\n {\n method: "POST",\n headers: {\n "X-Api-Key": API_KEY,\n "X-Tenant-Slug": TENANT_SLUG,\n "Content-Type": "application/json",\n },\n body: JSON.stringify({ persons }),\n }\n );\n return res.json();\n}\n\nasync function getGalleryStats(galleryToken) {\n const res = await fetch(\n `${API_BASE}/group-shoot/checkin/${galleryToken}/stats`,\n {\n headers: {\n "X-Api-Key": API_KEY,\n "X-Tenant-Slug": TENANT_SLUG,\n },\n }\n );\n return res.json();\n}\nIntegration checklist
- Get your API key from admin settings under Settings → API Keys
- Note your tenant slug and the gallery token from the admin URL
- Call GET /group-shoot/checkin/{token}/form-fields to discover which custom fields are configured and what their keys and labels are
- Before the session, POST /group-shoot/checkin/{token}/persons to pre-populate participants from your HR or CRM system
- Call GET /group-shoot/checkin/{token}/stats for a quick overview of gallery progress
- Call GET /group-shoot/checkin/{token}/persons to list all participants and use ?shootingDayId= to filter by day
- Use ?employeeId= to look up a specific employee directly
- Map customFields keys from the persons response using the field definitions from /form-fields
- Call GET /group-shoot/checkin/{token}/persons/{personId}/photos for their photos
- Use ?filter=favorites to get only the photos marked as favorite (for example HR profile pictures)
- Store or display url, thumbUrl, or miniUrl. Re-fetch after 24 hours when URLs expire
Downloading and storing photos (HR/CRM use case)
If you want to store photos permanently in your own system (for example employee profile pictures in an HR system), download them server-side immediately after fetching the URL.
Do not store the signed URL itself. It expires after 24 hours.
Recommended flow:
- Call GET /persons?employeeId=EMP-001 to find the person
- Call GET /persons/{personId}/photos?filter=favorites to get only their favorite photos (or omit the filter for all photos)
- Download the photo to your server using the url field (use photos[0] if you only need one)
- Store the downloaded file, not the URL
# Step 1 — get personId
PERSON_ID=$(curl -s "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/persons?employeeId=EMP-001" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT" \
| jq -r '.persons[0].personId')
# Step 2 — get photo URL
PHOTO_URL=$(curl -s "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/persons/$PERSON_ID/photos" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Tenant-Slug: YOUR_TENANT" \
| jq -r '.photos[0].url')
# Step 3 — download to your server
curl -o employee_photo.jpg "$PHOTO_URL"
import fs from "fs";
async function downloadEmployeePhoto(galleryToken, employeeId, outputPath) {
// 1. Find person by employeeId
const persons = await listPersons(galleryToken, employeeId);
const person = persons.persons?.[0];
if (!person) throw new Error("Employee not found");
// 2. Get photos
const result = await getPersonPhotos(galleryToken, person.personId);
const photo = result.photos?.[0];
if (!photo) throw new Error("No photos assigned yet");
// 3. Download and save
const res = await fetch(photo.url);
const buffer = await res.arrayBuffer();
fs.writeFileSync(outputPath, Buffer.from(buffer));
}
Photo downloads must happen server-side. The signed CDN URLs have CORS restrictions that prevent direct browser downloads from third-party origins.
Notes
- Write operations are limited to importing persons (POST /persons) and public check-in submissions (POST /group-shoot). Photo assignments and photo uploads can only be managed in the exportlab.io admin.
- Public check-in endpoints (GET /group-shoot/checkin/{token} and POST /group-shoot) do not require an API key, but they still require a valid tenant slug.
- Signed photo URLs expire after 24 hours. Always fetch fresh URLs, do not persist them.
- The code field is the participant personal access code for the self-service gallery. Do not expose it publicly.
- employeeId is optional. It is only present if provided at check-in or added by an admin.
- Favorites must be enabled per gallery in the admin settings (Settings → Favorite photos toggle). When disabled, isFavorite is always false and ?filter=favorites returns an empty array.
- Favorites can be set by the participant and by the admin. Both update the same favoriteFilenames list on the person record.