Location Cameras
Pair cameras, upload periodic JPEG captures, and manage location-scoped image history.
Each camera belongs to an organization and location. Each webcam needs its own identity and credential, even when several webcams share a Linux gateway. When cameras are enabled for the deployment, organization entitlements and the user’s LocationCameras permission determine access. Deployments running a restricted pilot may additionally limit availability to selected organizations. Document permissions do not grant camera-image access.
Pair a camera
Create a camera with POST /v1/spatial/locations/{locationId}/cameras, then request a one-use pairing code with POST /v1/cameras/{cameraId}/enrollments. Codes expire after 15 minutes. Do not put codes in URLs, logs, shell history or browser storage.
Before redemption, the device must durably save a random 32-byte lowercase hex credentialSecret and random UUID redemptionId. Send them with pairingCode in the JSON body of POST /v1/camera-ingestion/enrollments/redeem.
The response contains cameraId, credentialId, and assignmentGeneration. Subsequent requests use Authorization: Bearer pixcam1.<credentialId without hyphens>.<credentialSecret>. Retrying the same proposal recovers a lost response while the grant remains valid. Another proposal cannot reuse the grant. Successful re-pairing revokes the previous credential.
Device credentials work only on /v1/camera-ingestion/*; they cannot access user, document, worker or camera-management endpoints. Store them in files readable only by the gateway service user.
Upload a capture
- GET
/v1/camera-ingestion/configuration. Persist the configuration before acknowledging its revision. - Persist JPEG bytes, a new UUID capture ID, UTC capture time, assignment generation, exact size and SHA-256 atomically. Reuse this intent on retries.
- POST
/v1/camera-ingestion/uploads:
{
"captureId": "09f82e6c-7677-4314-a0ca-fd1adf687b1f",
"assignmentGeneration": 1,
"capturedAtUtc": "2026-09-20T10:00:00Z",
"sizeBytes": 340446,
"checksumSha256": "<64 lowercase hex characters>",
"clockQuality": "synchronized",
"contentType": "image/jpeg"
}- PUT the exact JPEG to
uploadUrlwith allrequiredHeaders. Do not send the camera bearer to object storage. The URL lasts five minutes; POST/v1/camera-ingestion/uploads/{operationId}/upload-urlrefreshes a pending transfer. - POST
/v1/camera-ingestion/uploads/{operationId}/complete. A202response means verification is pending. Poll the returnedstatusUrlwith the camera bearer. - Delete local bytes only after a
completedreceipt. S3 success,202, timeouts and lost responses are not publication acknowledgements.
The server verifies actual size, SHA-256 and a decoded single-frame JPEG, then writes a separate server-owned final object. Replaying a staging upload cannot overwrite a published image. Duplicate capture IDs recover the existing operation; changing immutable metadata returns 409.
Images consume Document storage capacity, including active reservations, but appear only in camera history. Defaults: 8 MiB per image, 24 megapixels, two pending uploads per camera, four new admissions per minute and 30 ingestion requests per minute per API instance. Honor 429/Retry-After and use exponential retry with jitter. Operations expire after 24 hours. Keep terminally rejected images for operator review; do not silently discard or reassign them.
Configuration and health
GET /v1/camera-ingestion/configuration includes nullable locationLatitude and
locationLongitude in decimal degrees for the camera's attached location.
Refresh these fields with configuration even when revision has not changed:
editing a location's coordinates does not change the camera revision.
PixCapture's optional Skip night setting uses these coordinates to calculate sunrise and sunset locally. If the location has no complete GPS pair, configure manual coordinates on the uploader. Only new captures pause overnight; queued uploads continue. Daylight follows the current date and location, including seasonal changes and polar day/night, without an external sunrise API.
PATCH /v1/cameras/{cameraId} with baseRevision to change pause, interval, retention, resolution, JPEG quality, timezone or optional local-time capture window. Stale revisions return 409. Defaults are 20 minutes, 1920×1080, quality 90 and 30-day retention.
POST /v1/camera-ingestion/heartbeat with appliedRevision, lastCaptureAtUtc, queue count/bytes, free bytes, oldest pending time and optional machine-readable errorCode. Last contact, last capture and last successful upload are independent health indicators.
Pause stops new capture but permits backlog uploads. Reduced retention requires confirmRetentionReduction: true and can delete existing images. Retention is measured from capture time, including offline delays. Use clockQuality: "estimated" when clock synchronization is uncertain. Captures over five minutes in the future or outside retention are rejected.
History and reassignment
To export retained original JPEGs for a camera, send
POST /v1/cameras/{cameraId}/exports?locationId={locationId} while signed in. The
response contains an operation ID; poll
GET /v1/cameras/{cameraId}/exports/{operationId}?locationId={locationId} until
status is ready, then follow downloadUrl. The ZIP is prepared in the
background and contains images completed before the request. Its entry names use
UTC capture times and capture IDs. GET /v1/cameras/{cameraId}/exports/latest?locationId={locationId}
resumes the current user's most recent operation. A changed or expired image set
requires a fresh export. Original-location camera read permission is checked on
every request; camera share links cannot export ZIP files.
GET /v1/spatial/locations/{locationId}/cameras lists available cameras. GET /v1/cameras/{cameraId}/captures?locationId={locationId} lists history. Follow hasNextPage, supplying the last item's capturedAtUtc as before and id as beforeId. GET /v1/camera-captures/{captureId}/download returns an expiring authorized URL; add thumbnail=true for previews. Each request checks the capture's original location.
POST /v1/cameras/{cameraId}/revoke revokes access and pauses capture. POST /v1/cameras/{cameraId}/retire also retires the camera; history remains until retention expires. DELETE /v1/camera-captures/{captureId} hides an image immediately and releases capacity after object cleanup.
Before moving a camera, pause it and wait for acknowledgement and empty local/server queues. POST baseRevision and target locationId to /v1/cameras/{cameraId}/relinks, then re-enroll. Explicit cancelPending: true cancels unfinished server operations; retain local images for review. Earlier images remain discoverable at their original location as camera history. Cross-organization moves require a new camera identity.
Deleting a location retires its current cameras, revokes their credentials and queues cleanup of its captures. Cleanup receipts survive location deletion so reservations and objects can be released correctly.
Timeline browsing
GET /v1/cameras/{cameraId}/timeline?locationId={locationId} returns first and
last capture metadata (both null for an empty archive). These bounds support a
date/time slider without downloading or counting the complete archive.
The response also includes gaps: empty periods with UTC start and end capture
timestamps. Use a compressed time scale to draw these as short striped sections;
keep displaying the actual capture timestamps. Normal capture intervals are not gaps:
the threshold is at least 15 minutes and five times the local capture cadence.
Cadence is estimated from neighboring consecutive captures on both sides, using
the slower side when the recording interval changes. Regular half-hourly captures
remain expanded, including after a switch from faster recording. Without enough
neighboring evidence an isolated interval is not classified as an outage.
Detection uses 512 time bins with indexed boundary lookups, returning at most the
128 largest gaps. Boundaries are exact; smaller gaps wholly inside one time bin
may be omitted in very long archives. No complete capture list is loaded or counted.
Results are cached for up to 30 seconds per camera/location and upload watermark;
new completed uploads (including uploads into earlier gaps) use a new cache key.
Authorization is checked for each request. Shared timelines include the same gap
metadata for their selected camera only.
GET /v1/cameras/{cameraId}/captures/seek?locationId={locationId}&at={utcTimestamp}
returns the nearest retained capture, or null if none remain. at is an ISO 8601
timestamp with timezone. Gaps snap to an actual frame; equal distances prefer the
earlier frame. For exact previous/next stepping, add direction=older or newer
and supply the current capture's timestamp as at and its ID as captureId.
The ID resolves frames with identical timestamps. At an archive edge the result
is null. Both endpoints require read permission at the requested original location;
deleted, expired and unfinished captures are excluded.
Throttle and cancel intermediate seeks during dragging. Fetch thumbnail download URLs while scrubbing, then request the original when the user settles on a frame. The selected frame's timestamp can differ from the requested time across gaps.
Tiny timeline previews
Use POST /v1/cameras/{cameraId}/previews?locationId={locationId} to load
small previews for several points on a timeline in one request:
{ "times": ["2026-09-21T12:00:00Z", "2026-09-21T12:30:00Z"] }Send 1–32 timestamps. The response contains samples in request order, each with
at and the nearest retained captureId (or null), and images, each with
minimal capture metadata and jpegBase64. Each distinct capture is encoded once,
even if several sample times resolve to it. Decode with data:image/jpeg;base64,
plus jpegBase64. The displayed timestamp must come from the returned capture.
A preview is a deliberately soft JPEG: longest edge at most 128 pixels, quality 25, and at most 3072 bytes before base64 encoding. Extremely detailed images may be reduced further to stay within that byte limit. Gallery thumbnails remain 640 pixels; originals are unchanged. New captures receive previews automatically. Older captures use a small version derived from their existing thumbnail.
For shared links, use POST /v1/shared-cameras/{cameraId}/previews with the same
body and the X-Camera-Share header. Batches enforce current location permissions,
share selection, revocation and capture retention, including when preview bytes
are cached by the service. Both endpoints return Cache-Control: no-store and
allow 120 batches per minute per IP per API instance; honor 429 and Retry-After.
Preload a bounded set of samples rather than the entire archive. PixAtlas loads 64 overview samples in batches of 16, refines the exact hover time and neighboring samples on demand, and retains at most 128 tiny images in viewer memory. Moving or closing the viewer cancels obsolete requests. On a jump, a cached preview can appear immediately, followed by the regular thumbnail and full image. Do not persist shared previews or cache them in a service worker.
Share selected cameras
In the image archive, choose Kameras teilen, create a named link and select its cameras. Anyone with the link can view those cameras' retained and future images at this location without signing in. Use the same dialog to edit the selection or revoke the entire link. Editing the selection keeps the link unchanged. Moving a camera to another location does not expose images from its new location.
API clients can manage links with:
GET /v1/spatial/locations/{locationId}/camera-shares: current share permissions and manageable links.POST /v1/spatial/locations/{locationId}/camera-shares: create a link with{ "name": "Baufortschritt", "cameraIds": ["<camera UUID>"] }.PATCH /v1/camera-shares/{id}: replace name and camera selection; include the returnedrevisionasbaseRevision.POST /v1/camera-shares/{id}/revoke: revoke the entire link with{ "baseRevision": 1 }.
Creation requires read access to the cluster, location and cameras plus
ExternalShareLinksCreate. Editing and revoking require ExternalShareLinksManage.
Concurrent edits return 409; reload before applying changes. A link selects
1–100 cameras, and a location can have up to 100 active links.
Treat the returned token as a secret. Send it in the X-Camera-Share header on
every anonymous request; do not send account or camera-ingestion credentials:
GET /v1/shared-cameras: the current link name, revision and selected cameras.GET /v1/shared-cameras/{cameraId}/timeline: first and latest retained captures.GET /v1/shared-cameras/{cameraId}/captures/seek?at={timestamp}&direction=nearest: seek a frame. Useolder/newerplus the exact timestamp andcaptureIdfor stepping.GET /v1/shared-cameras/captures/{captureId}/image?thumbnail=true: download a JPEG thumbnail. Omitthumbnailfor the full image.
Image responses contain bytes, not reusable storage URLs. Each request checks the
current link and selection. Revoked links, removed cameras and unavailable images
return 404. Responses are no-store; do not cache them. Anonymous reads allow
600 requests per minute per IP per API instance; honor 429 and Retry-After.
The PixAtlas viewer refreshes a link's selection every 15 seconds. Revocation blocks
subsequent requests, but cannot recall images already downloaded or in flight.
Latest images in camera lists
Use POST /v1/spatial/locations/{locationId}/cameras/latest-previews to populate
camera cards without requesting each camera's history:
{ "cameraIds": ["11111111-1111-4111-8111-111111111111", "22222222-2222-4222-8222-222222222222"] }Send 1–32 distinct, non-empty IDs from the visible portion of the camera list.
Split larger lists into batches as they scroll into view. The response items
preserves request order; each item has cameraId, capture, jpegBase64 and
thumbnailUrl. Empty, unavailable and out-of-scope cameras have null image fields.
The latest retained completed image is ordered by capture time, then ID descending.
History remains scoped to the original location even if a camera has moved.
Tiny JPEGs have a longest edge of at most 128 pixels and a 3072-byte limit. Paint
them first; download the expiring thumbnail URL only after the card stays visible.
Original images are never returned by this endpoint. Cancel offscreen work and
keep only a bounded, short-lived in-memory cache. The endpoint requires camera
read permission and enabled cameras; it rechecks authorization and retention
after preparing previews. Responses use Cache-Control: no-store. The camera
preview rate limit applies; honor 429 and Retry-After.