Exportlab logoDocs

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.

Get check-in infoGET
/group-shoot/checkin/{token}

Returns public metadata needed for a custom check-in page.

Submit check-inPOST
/group-shoot

Creates a person from a public check-in form.

List personsGET
/group-shoot/checkin/{token}/persons

Returns all participants registered for a group shoot gallery.

Import persons (bulk)POST
/group-shoot/checkin/{token}/persons

Create multiple persons in one request.

Get photos for a personGET
/group-shoot/checkin/{token}/persons/{personId}/photos

Returns all photos assigned to a specific person with signed CDN URLs.

List shooting daysGET
/group-shoot/checkin/{token}/days

Returns all shooting days configured for a gallery.

Get check-in form fieldsGET
/group-shoot/checkin/{token}/form-fields

Returns the configured check-in form fields for a gallery.

Get gallery statsGET
/group-shoot/checkin/{token}/stats

Returns aggregated stats for a group shoot gallery.

Base URL

text
Copied
https://api.exportlab.io/v1

Authentication

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.

HeaderDescription
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.

json
Copied
{
  "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.

json
Copied
{
  "ok": false,
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Max 60 requests per minute. Retry after 42s.",
  "requestId": "..."
}
HeaderDescription
X-RateLimit-LimitMax requests allowed per window (60)
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix 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.

bash
Copied
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}" \
  -H "X-Tenant-Slug: YOUR_TENANT"
json
Copied
{
  "ok": true,
  "galleryName": "Team Headshots – Spring 2026",
  "selfieEnabled": true,
  "tenantSlug": "acme",
  "token": "iURWUkHo",
  "requestId": "a41c49f0-..."
}
Path parameterTypeDescription
tokenstringThe gallery token
Response fieldTypeDescription
galleryNamestringGallery display name
selfieEnabledbooleanWhether the check-in flow accepts a selfie upload
tenantSlugstringTenant slug resolved for this request
tokenstringGallery 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.

json
Copied
{
  "action": "submitCheckin",
  "token": "iURWUkHo",
  "name": "Max Mustermann",
  "email": "max@example.com",
  "employeeId": "EMP-001",
  "selfieBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
}
bash
Copied
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"
  }'
json
Copied
{
  "ok": true,
  "personId": "mZTZBszq",
  "code": "LWR7",
  "name": "Max Mustermann",
  "requestId": "b4ad3a02-..."
}
FieldTypeRequiredDescription
actionstringYesMust be submitCheckin
tokenstringYesGallery token
namestringYesFull name (1–200 chars)
emailstringNoEmail address (max 200 chars)
employeeIdstringNoHR system employee ID (max 100 chars)
selfieBase64stringNoBase64-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.

bash
Copied
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"
json
Copied
{
  "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 parameterTypeDescription
tokenstringThe gallery token (visible in the admin URL)
Query parameterTypeRequiredDescription
employeeIdstringNoFilter to return only the person with this employee ID
shootingDayIdstringNoFilter to return only persons assigned to this shooting day ID
limitnumberNoPage size, 1-100. Default: 100
cursorstringNoPagination cursor from the previous response's nextCursor
Person fieldTypeDescription
personIdstringUnique person ID within this gallery
namestringFull name
emailstring | nullEmail address if provided at check-in
employeeIdstring | nullHR system employee ID if provided
shootingDayIdstring | nullID of the assigned shooting day. null if no day is assigned
selectedPackageIdstring | nullID of the selected package, if any
codestring4-character personal access code (example: LWR7)
photoCountnumberNumber of photos assigned to this person
checkedInAtISO 8601 | nullWhen the person checked in. null if added manually by admin
createdAtISO 8601When the record was created
customFieldsobject | —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.

json
Copied
{
  "persons": [
    { "name": "Max Mustermann", "email": "max@example.com", "employeeId": "EMP-001", "shootingDayId": "abc12345" },
    { "name": "Erika Musterfrau", "employeeId": "EMP-002" },
    { "name": "John Doe" }
  ]
}
bash
Copied
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" }
    ]
  }'
json
Copied
{
  "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" }
  ]
}
json
Copied
{
  "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 parameterTypeDescription
tokenstringThe gallery token
FieldTypeRequiredDescription
personsarrayYesList of persons to import (max 500)
persons[].namestringYesFull name (1–200 chars)
persons[].emailstringNoEmail address (max 200 chars)
persons[].employeeIdstringNoHR system employee ID (max 100 chars)
persons[].shootingDayIdstringNoShooting day ID from GET /days. Invalid IDs are silently ignored
Response fieldTypeDescription
importednumberNumber of persons successfully created
skippednumberNumber of rows skipped due to validation errors
personsarrayCreated persons with their generated code and personId
skippedDetailsarrayOnly present when skipped > 0 — lists index and reason for each skipped row
Skip reasonDescription
name_requiredname field is missing or empty
code_generation_failedCould not generate a unique code after 10 attempts
write_failedDatabase 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.

bash
Copied
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"
json
Copied
{
  "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 parameterTypeDescription
tokenstringThe gallery token
personIdstringThe person ID from the list endpoint
Query parameterTypeRequiredDescription
filterstringNoUse favorites to return only photos marked as favorite
Photo fieldTypeDescription
filenamestringOriginal filename
isFavoritebooleanWhether this photo is marked as favorite
urlstringSigned URL, full resolution original (valid 24h)
thumbUrlstringSigned URL, thumbnail variant, ~512px wide (valid 24h)
miniUrlstringSigned URL, mini variant, ~128px wide (valid 24h)
widthnumber | nullWidth in pixels
heightnumber | nullHeight in pixels
sizenumber | nullFile size in bytes
takenAtISO 8601 | nullEXIF capture time if available
createdAtISO 8601Upload timestamp
Person fieldTypeDescription
personIdstringUnique person ID within this gallery
namestringFull name
employeeIdstring | nullHR system employee ID if provided
favoriteCountnumberNumber 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.

bash
Copied
curl -X GET "https://api.exportlab.io/v1/group-shoot/checkin/{TOKEN}/days"
json
Copied
{
  "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 parameterTypeDescription
tokenstringThe gallery token
Response fieldTypeDescription
shootingDaysarrayAll shooting days for this gallery, sorted by date ascending
shootingDays[].idstringShooting day ID (used as shootingDayId on persons)
shootingDays[].datestringDate in YYYY-MM-DD format
shootingDays[].labelstring | nullOptional display label
shootingDays[].startTimestring | nullStart time in HH:MM format
shootingDays[].endTimestring | nullEnd time in HH:MM format
shootingDays[].maxPersonsnumber | nullCapacity limit, or null for unlimited
shootingDays[].notesstring | nullOptional admin notes visible in the check-in flow
shootingDays[].spotsRemainingnumber | nullSpots still available (null if unlimited)
shootingDays[].createdAtstringISO 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.

bash
Copied
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"
json
Copied
{
  "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 parameterTypeDescription
tokenstringThe gallery token
FieldTypeDescription
keystringField identifier. Built-in: employeeId, phone, department, notes. Custom: custom-{uuid}
labelstringDisplay label as configured by the admin
typestringtext, textarea, number, or select
requiredbooleanWhether the field must be filled in
optionsarrayOnly present when type is select. Each entry has value and label

URL aliases

Both URL patterns are equivalent and fully supported.

MethodPatternDescription
GET/group-shoot/checkin/{token}/form-fieldsPrimary
GET/groupshoot/{token}/form-fieldsShort alias
GET/group-shoot/checkin/{token}/daysPrimary
GET/groupshoot/{token}/daysShort alias
GET/group-shoot/checkin/{token}/statsPrimary
GET/groupshoot/{token}/statsShort alias
GET/group-shoot/checkin/{token}/personsPrimary
GET/groupshoot/{token}/personsShort alias
POST/group-shoot/checkin/{token}/personsPrimary (import)
POST/groupshoot/{token}/personsShort alias (import)
GET/group-shoot/checkin/{token}/persons/{personId}/photosPrimary
GET/groupshoot/{token}/persons/{personId}/photosShort alias

Error codes

HTTPerrorDescription
400missing_tenant_slugX-Tenant-Slug header is missing
400invalid_tenantTenant slug could not be resolved
400not_a_groupshoot_galleryThe gallery token exists but is not a group shoot
400token_requiredGallery token is missing
400name_requiredName is missing or empty
400invalid_jsonRequest body is not valid JSON
401api_key_requiredNo API key provided
401invalid_api_keyAPI key is wrong, revoked, or disabled
404gallery_not_foundGallery token does not exist
404person_not_foundPerson ID does not exist in this gallery
405method_not_allowedOnly GET is supported
409shooting_day_fullThe selected shooting day has reached its maxPersons capacity
429rate_limit_exceededToo many requests. See X-RateLimit-Reset header
500internal_errorServer 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.

js
Copied
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}\n

Integration 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
bash
Copied
# 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"
js
Copied
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.