Fotobox kiosk integration

Proofing Gallery provides an authenticated provisioning and JPEG upload API. The existing kiosk software is not changed by this feature; this document defines its future communication contract. The OpenAPI specification describes the wire format.

Choose New project → Fotobox, enter an event title, select a writable parent folder and a starting design, then choose Create and publish. Optional access settings specify a gallery password and expiration date. Nextcloud can require either setting. The new empty gallery is immediately public and shows a waiting state. Copy its public link or copy/download the connection JSON for the kiosk integration. Account credentials and gallery passwords are not included in that JSON.

The API and this dialog run the same provisioning workflow. User preferences supply a default parent folder and design. Saved designs contribute presentation settings; the Fotobox workflow explicitly sets a shared standard delivery, direct folder listing, newest photos first, individual downloads where permitted, and disables guest uploads and collaboration. This is separate from the existing Event Delivery feature that distributes private recipient albums.

Communication and architecture

sequenceDiagram
    participant K as Existing kiosk (future integration)
    participant A as Proofing Gallery OCS API
    participant N as Nextcloud files and shares
    K->>A: GET kiosk/setup (account + app password)
    A-->>K: Defaults, capabilities and sharing policy
    K->>A: POST kiosk/galleries (durable eventId)
    A->>N: Create folder, gallery and public share
    A-->>K: galleryId, galleryUrl, upload configuration
    loop For each photo
        K->>A: PUT galleries/{galleryId}/photos/{photoId} (JPEG)
        A->>N: Stage and commit file
        A-->>K: stored, fileId, photoUrl
        Note over K: Persist response, generate QR from photoUrl
    end

Use HTTPS and a dedicated Nextcloud account with an app password. The account must own the created gallery and have a writable parent folder and permission to publish. Kiosk calls use HTTP Basic authentication, OCS-APIRequest: true, and ?format=json. Browser calls use the current Nextcloud session. There is no separate upload secret and no unauthenticated upload API. Setup and upload do not grant guests write access.

All paths are relative to the Nextcloud installation root, including a deployment subdirectory if present:

Method OCS path Purpose
GET /ocs/v2.php/apps/proofing_gallery/api/v1/kiosk/setup Read defaults, capabilities, sharing policy and upload limits
POST /ocs/v2.php/apps/proofing_gallery/api/v1/kiosk/galleries Create folder, gallery and immediately usable public share
PUT /ocs/v2.php/apps/proofing_gallery/api/v1/kiosk/galleries/{galleryId}/photos/{photoId} Store one raw JPEG and return its photo-page URL

Do not guess the upload URL: use the returned upload.urlTemplate, replacing the exact upload.photoIdPlaceholder (PHOTO_ID) with the durable UUID. The response’s galleryUrl and photoUrl are absolute URLs generated by Nextcloud. Configure its external host/proxy URL correctly.

Provisioning request and durable event identity

eventId is required, account-scoped, 1–128 URL-safe characters (A-Z, a-z, digits, ., _, :, -; first character alphanumeric). Persist it before the request. title is required, 1–255 characters after trimming. parentFolderId and designPresetId may be omitted or null to use owner preferences. designPresetId: 0 selects the studio default; a positive ID selects an owned design. A writable parent folder is required. password defaults to an empty string; expiresAt defaults to an empty string and otherwise uses a future YYYY-MM-DD date. Nextcloud policy can impose a password, default expiration or maximum expiration.

The same account, event ID and normalized request parameters resume an incomplete setup or return the same ready gallery. Changed request parameters return 409. Defaults are frozen at the first successful reservation; changes to owner preferences do not create another gallery on replay. Preserve the original request, including optional field choices and password, for retries. Identity records remain until gallery purge or account removal; they have no 24-hour timeout. Use a new event ID for a new event. Revoked, expired, archived or differently scoped shares are not silently replaced on replay.

Upload request and response

Use a stable UUID photoId for each captured/exported JPEG. Persist the ID and exact JPEG bytes locally before uploading. Send Content-Type: image/jpeg with raw bytes, without multipart encoding. V1 accepts JPEG only, validates the image format and enforces maxBytes. Photos are stored as {photoId}.jpg in the event folder. UUIDs are normalized to lowercase.

A successful first request returns HTTP 201. An identical replay returns HTTP 200 and the same file ID and URL. Different bytes for an existing photo ID return 409 and never overwrite the photo. A stored file left by an interrupted commit is recovered by deterministic filename and checksum. Uploads are serialized per gallery. The API returns success only after the file and receipt are stored and the current public link covers that photo. Preview generation can finish later.

An external file deletion or share change can invalidate a previous URL; an API error does not restore deleted media or resurrect revoked links. Keep original files and the kiosk’s successful receipts according to the operator’s retention settings.

OCS wraps JSON responses; read ocs.data, not top-level photoUrl:

{
  "ocs": {
    "meta": { "status": "ok", "statuscode": 201, "message": "OK" },
    "data": {
      "photoId": "df53f798-29cf-4f6d-b18a-859a9c1f7632",
      "fileId": 12345,
      "status": "stored",
      "photoUrl": "https://cloud.example.test/s/SHARE_TOKEN?photo=12345",
      "replayed": false
    }
  }
}

The URL opens the gallery lightbox for that Nextcloud file ID. It is not a direct JPEG or download URL. Use photoUrl unchanged as the QR payload. A password-protected share still prompts guests for its password; the URL contains no password. All guests with access can browse the same gallery. A photo QR does not provide private per-photo access.

Visible Fotobox galleries refresh every five seconds without changing the active photo. Hidden tabs pause updates and refresh when visible again. Direct folder listing avoids waiting for the recursive index cron job. Under normal load, new photos should appear within ten seconds; background processing and network conditions can increase this time.

Failures, retries and offline queue

HTTP Meaning Kiosk behavior
200 / 201 Successful replay / first stored result Persist ocs.data, then generate QR
401 Authentication rejected Correct account/app password
403 Policy or permission denied Operator intervention
404 Owned gallery, file or share unavailable Check event configuration
409 Event parameters/photo bytes conflict, or public link no longer usable Reconcile state; do not overwrite or invent another photo ID for the same capture
422 Invalid parameters, MIME/size, sharing or password policy Correct configuration or image
423 Gallery busy Honor Retry-After (currently 1 second), retry same ID and bytes
429 60 new uploads/minute per gallery Honor Retry-After (currently 60 seconds), retain queue
Network / 5xx Result uncertain Retry original event request or original photo ID and bytes with backoff

Successful photo replays do not consume the new-photo rate limit. Queued captures need durable local storage across kiosk restarts, bounded retries with backoff and visible queue/error state. Do not generate a success QR until an upload acknowledgement supplies photoUrl. Store acknowledgement and QR source together; never derive a URL from the local filename. No kiosk executable or source change is supplied here.

Curl examples

Set NEXTCLOUD_URL, NEXTCLOUD_USER and NEXTCLOUD_APP_PASSWORD in your own environment. These examples require curl and jq. Save the request JSON and event ID before making the provisioning call.

curl --fail-with-body --user "$NEXTCLOUD_USER:$NEXTCLOUD_APP_PASSWORD" \
  -H 'OCS-APIRequest: true' \
  "$NEXTCLOUD_URL/ocs/v2.php/apps/proofing_gallery/api/v1/kiosk/setup?format=json"

# Use the parent folder ID from setup, or supply another writable folder ID.
cat > event-request.json <<'JSON'
{"eventId":"event-2026-10-01-001","title":"Autumn event","parentFolderId":123}
JSON
curl --fail-with-body --user "$NEXTCLOUD_USER:$NEXTCLOUD_APP_PASSWORD" \
  -H 'OCS-APIRequest: true' -H 'Content-Type: application/json' \
  --data-binary @event-request.json \
  "$NEXTCLOUD_URL/ocs/v2.php/apps/proofing_gallery/api/v1/kiosk/galleries?format=json" \
  --output event-response.json
jq '.ocs.data' event-response.json > connection.json

# Persist this UUID together with the original JPEG before the first request.
PHOTO_ID='df53f798-29cf-4f6d-b18a-859a9c1f7632'
UPLOAD_URL=$(jq -r --arg id "$PHOTO_ID" '.upload.urlTemplate | sub("PHOTO_ID"; $id)' connection.json)
curl --fail-with-body --user "$NEXTCLOUD_USER:$NEXTCLOUD_APP_PASSWORD" \
  -H 'OCS-APIRequest: true' -H 'Content-Type: image/jpeg' \
  -X PUT --data-binary @photo.jpg "$UPLOAD_URL" --output photo-response.json
jq -r '.ocs.data.photoUrl' photo-response.json

Storage and lifecycle

A compatible migration adds account/event mappings and upload receipts with unique identities. Per-event and per-gallery locks prevent concurrent duplicates. Provisioning saves its folder mapping and commits gallery/public-link mappings transactionally; uploads stage and commit in Nextcloud storage, then persist the receipt. A retry resumes those boundaries. The stored configuration contains no gallery/app password; the request fingerprint is an HMAC-SHA-256 digest keyed by the Nextcloud instance secret and is excluded from privacy export. Privacy export/purge includes kiosk records. Files follow the existing gallery/files lifecycle. Permanently purging a gallery removes its kiosk identity, so clients must not continue replaying that deleted event.