PLDashboard

API Reference

HTTP endpoints for photo uploads and integration with getpic.app.

1. Authentication

Pass your project API key in the HTTP request header:

x-api-key: YOUR_API_KEY

Generate keys in Dashboard → Project Settings → API Keys.

POST

/api/upload

Standard single-file upload via multipart/form-data.

ParameterTypeRequiredDescription
fileFileYesBinary photo data (JPEG, PNG, RAW)
destinationstringYes'individual' (Photos 1:1) | 'gallery' | 'selection_preview'
slugstringIf galleryTarget gallery slug identifier
Request Examples
Response Example (200 OK)
{
  "success": true,
  "photoId": "550e8400-e29b-41d4-a716-446655440000",
  "shortCode": "abc123xy",
  "url": "https://your-domain.com/s/abc123xy",
  "canonicalUrl": "https://your-domain.com/s/550e8400-e29b-41d4-a716-446655440000",
  "filename": "photo.jpg",
  "r2Key": "tenant-id/galleries/gallery-id/photo.jpg"
}
RAW / Stream

Multipart Upload (/api/upload/multipart/*)

3-step chunked upload flow for large files or RAW camera formats:

1. INIT
POST /api/upload/multipart/init
Body: { destination, slug, filename }
2. PART
POST /api/upload/multipart/part
Params: rawR2Key, uploadId, partNumber
3. COMPLETE
POST /api/upload/multipart/complete
Body: { photoId, parts: [{ partNumber, etag }] }
// 1. Init session
const init = await fetch('/api/upload/multipart/init', {
  method: 'POST',
  headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ destination: 'gallery', slug: 'wedding-2026', filename: file.name }),
}).then(r => r.json());

// 2. Upload chunk parts
const parts = [];
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
let partNumber = 1;

for (let start = 0; start < file.size; start += CHUNK_SIZE) {
  const chunk = file.slice(start, start + CHUNK_SIZE);
  const partUrl = `/api/upload/multipart/part?rawR2Key=${encodeURIComponent(init.rawR2Key)}&uploadId=${encodeURIComponent(init.uploadId)}&partNumber=${partNumber}`;

  const { etag } = await fetch(partUrl, {
    method: 'POST',
    headers: { 'x-api-key': API_KEY },
    body: chunk,
  }).then(r => r.json());

  parts.push({ partNumber, etag });
  partNumber++;
}

// 3. Complete session
const result = await fetch('/api/upload/multipart/complete', {
  method: 'POST',
  headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    photoId: init.photoId,
    rawR2Key: init.rawR2Key,
    uploadId: init.uploadId,
    parts,
    destination: 'gallery',
    slug: 'wedding-2026',
    filename: file.name,
    size: file.size,
  }),
}).then(r => r.json());

4. Legacy Routes

Legacy EndpointMethodStandard Equivalent
/api/upload/photoPOSTPOST /api/upload (destination='individual')
/api/upload/selection-previewPOSTPOST /api/upload (destination='selection_preview')
/api/upload/gallery/:slugPOSTPOST /api/upload (destination='gallery' & slug)