---
title: "Upload a new asset"
method: POST
path: "/api/assets"
tags: ["assets"]
---

# Upload a new asset

`POST /api/assets`

Uploads a new asset (image or video) and its metadata as multipart form data, returning the created asset with 201. Uploads are deduplicated per library by the file's SHA-256 checksum: re-uploading a file whose bytes already exist in the target library stores nothing and returns the existing asset with 200. Storage caps are checked before the duplicate lookup, so an upload is refused with 507 whenever the account or the target library is already at its storage cap — even when the bytes would have deduplicated to an existing asset. A transient upstream storage error returns 502 — retryable after the `Retry-After` interval. When `library_id` is omitted and no default library can be chosen (the account has multiple live libraries), the request is refused with 400. Image metadata is extracted before the response returns; the rest of processing (thumbnails, search indexing, face detection, and video metadata extraction) continues asynchronously after the response.

## Response `200`

The file already exists in the target library (matched by SHA-256 checksum); the existing asset is returned and nothing new is stored.

- AssetResponse — Represents a photo or video asset with metadata and access URLs. Top-level fields describe the asset's current rendering (the version identified by `current_version_id`) unless they explicitly name the original upload, like `original_file_name` and the `file_data` group.
  - `id` string, required — Unique asset identifier with 'asset_' prefix
  - `mime_type` string, required — MIME type of the current rendering (e.g., 'image/jpeg', 'video/mp4').
  - `original_file_name` string, required — Filename the asset was uploaded under.
  - `local_datetime` string, date-time, required — When the photo/video was taken, in the device's local timezone
  - `file_data` FileDataResponse — File/provenance scalars describing the uploaded *file* (not its content). Returned only when requested via ``include=file_data``; the whole object is ``null`` otherwise. When present, every field carries its real value — ``checksum_sha1`` is the lone exception (``null`` for legacy rows that never had a SHA-1). This nested object is the home for the file/provenance group.
    - `device_asset_id` string, required — Original asset identifier from the device that uploaded this asset.
    - `device_id` string, required — Identifier of the device that uploaded this asset.
    - `file_created_at` string, date-time, required — When the file was created on the uploading device.
    - `file_modified_at` string, date-time, required — When the file was last modified on the uploading device.
    - `checksum` string, required — Base64-encoded SHA-256 hash of the asset contents for duplicate detection and integrity.
    - `checksum_sha1` string, nullable — Base64-encoded SHA-1 hash of the asset contents. `null` for older assets that have no SHA-1.
    - `file_size_bytes` integer, required — Size of the uploaded file in bytes. Each rendering's own size is on its row in the asset's version listing.
  - `created_at` string, date-time, required — When this asset record was created in the database
  - `updated_at` string, date-time, required — When this asset record was last updated
  - `metadata` MetadataResponse — Metadata for an asset — camera/EXIF fields, GPS, and location names.
    - `asset_id` string, required — ID of the asset this metadata belongs to
    - `created_at` string, date-time, required — When this metadata record was created
    - `updated_at` string, date-time, required — When this metadata record was last updated
    - `make` string, nullable — Camera manufacturer (e.g., 'Canon', 'Nikon')
    - `model` string, nullable — Camera model (e.g., 'EOS 5D Mark IV')
    - `orientation` integer, nullable — Image orientation value (1-8) indicating rotation/flip: 1=normal, 2=mirror horizontal, 3=rotate 180°, 4=mirror vertical, 5=mirror horizontal+rotate 90° CW, 6=rotate 90° CW, 7=mirror horizontal+rotate 90° CCW, 8=rotate 90° CCW
    - `raw_width` integer, nullable — Pre-rotation raw width; null when not available
    - `raw_height` integer, nullable — Pre-rotation raw height; null when not available
    - `modified_datetime` string, date-time, nullable — When the file was last modified, with timezone offset if available
    - `original_datetime` string, date-time, nullable — When the photo was originally taken, with timezone offset if available
    - `digitized_datetime` string, date-time, nullable — When the photo was digitized, with timezone offset if available
    - `lens_model` string, nullable — Lens model used (e.g., 'EF 24-70mm f/2.8L II USM')
    - `f_number` number, nullable — Aperture f-stop value (e.g., 2.8, 5.6)
    - `focal_length` number, nullable — Focal length in millimeters
    - `iso` integer, nullable — ISO sensitivity value (e.g., 100, 800, 3200)
    - `exposure_time` number, nullable — Shutter speed in seconds (e.g., 0.001 for 1/1000s)
    - `exposure_bias` number, nullable — Exposure compensation in EV (e.g., -1.0, +0.5)
    - `latitude` number, nullable — GPS latitude in decimal degrees
    - `longitude` number, nullable — GPS longitude in decimal degrees
    - `altitude` number, nullable — GPS altitude in meters
    - `city` string, nullable — City name
    - `state` string, nullable — State/province name
    - `country` string, nullable — Country name
    - `country_code` string, nullable — ISO 3166-1 alpha-2 country code (e.g., 'US', 'JP')
    - `sublocation` string, nullable — Neighborhood or district
    - `place_name` string, nullable — Landmark or point-of-interest name
    - `timezone` string, nullable — IANA timezone identifier (e.g., 'America/Los_Angeles')
    - `display_label` string, nullable — Human-readable location label. Picks the most specific available identifier (place_name > sublocation > city > country) and appends broader context (city, then state-or-country). Example: 'Golden Gate Bridge, San Francisco, California'. Null when no location fields are populated.
    - `description` string, nullable — Image description or caption
    - `fps` number, nullable — Frame rate for video files
    - `live_photo_cid` string, nullable — Live photo content identifier
    - `projection_type` string, nullable — Projection type (e.g., for 360° photos)
    - `auto_stack_id` string, nullable — Identifier for automatic photo stacking
    - `rating` integer, nullable — User or camera rating (typically 1-5 stars)
  - `metrics` object, nullable — ML-generated quality scores and other metrics. `null` when not requested via `include=metrics`.
  - `asset_urls` object, nullable — Named asset variants. Images: 'original', 'thumbnail', 'small', 'preview', 'fullsize'. Videos: 'original', plus 'thumbnail_image', 'small_image', 'preview_image', 'fullsize_image' pointing at the extracted still. 'original' is served with a Content-Disposition attachment header (signed 'dl' filename param) so a top-level navigation saves it to disk, while inline subresource loads (<video>, fetch) still render it. Variant URLs are stable: a derived variant may briefly 404 until its artifact is generated, then serve from the same URL.
  - `description` string, nullable — AI-generated description of the asset's content, quality, and composition. null means description generation has not yet run; empty string means the model refused to describe the asset. Distinct from metadata.description (camera-embedded EXIF metadata).
  - `thumbhash` string, nullable — Base64-encoded ThumbHash placeholder (~28 chars). Clients decode with the `thumbhash` library (JS / Swift / Kotlin) to render an instant blurred preview before the CDN thumbnail arrives. `null` while generation is pending.
  - `faces` FaceResponse[], nullable — All faces detected in this asset. `null` when not requested via `include=faces`; `[]` when requested but the asset has no faces.
    - `id` string, required — Unique face identifier with 'face_' prefix
    - `asset_id` string, required — ID of the asset containing this face
    - `person_id` string, nullable — ID of the person this face belongs to (if identified)
    - `bounding_box` object, required — Face location as {x, y, w, h} coordinates in pixels
    - `confidence` number, nullable — Detector confidence on a 0-1 scale; higher is more confident among faces detected under the same configuration (values are not comparable across detector generations). Null on legacy faces without a stored score and on manually added faces.
    - `source` 'automatic' | 'manual', required — How this face was added: 'automatic' for detector-found faces, 'manual' for user-drawn face boxes.
    - `timestamp_ms` integer, nullable — For video files, timestamp in milliseconds when face appears
    - `asset_urls` object, nullable — Asset variants for this face: 'thumbnail' with face crop
    - `created_at` string, date-time, required — When this face was detected and recorded
    - `updated_at` string, date-time, required — When this face record was last updated
    - `cluster_assignment` ClusterAssignmentResponse — Per-face cluster-assignment diagnostics: how well the face fits its currently-assigned Person, and which other Persons are nearby in embedding space. Surfaced via ``include=cluster_assignment`` on the faces endpoints — used by the operator-facing face cleanup dashboard to triage mis-clustered faces.
      - `distance_to_person` number, nullable — Cosine distance from the face's embedding to its currently-assigned Person's centroid. Lower = better fit. Null when the face is unassigned or when the assigned Person has no centroid.
      - `candidates` FaceCandidatePersonResponse[] — Persons in the same library that pass the same gate shape as production face assignment, surfaced with deliberately relaxed thresholds so the list is a superset of what the automated path would admit. Sorted ascending by distance. Excludes the face's currently-assigned Person (its distance is in `distance_to_person`). Empty when no eligible Persons pass the gate.
        - `person_id` string, required — Person ID (with 'person_' prefix) of the candidate.
        - `name` string, nullable — Display name of the candidate Person, or null for unnamed clusters. Candidates surface the same Persons production assignment considers, which includes unnamed clusters.
        - `distance` number, required — Cosine distance from the face's embedding to this Person's centroid (lower = closer).
  - `people` PersonResponse[], nullable — All unique people identified in this asset (deduplicated from faces). `null` when not requested via `include=people`; `[]` when requested but none are identified.
    - `id` string, required — Unique person identifier with 'person_' prefix
    - `name` string, nullable — Optional name assigned to this person
    - `birth_date` string, date, nullable — Optional birth date of this person
    - `is_hidden` boolean, required — Whether this person should be hidden from the UI
    - `is_favorite` boolean, required — Whether this person is marked as a favorite
    - `asset_count` integer, nullable — Number of unique photos this person appears in, or null if not computed
    - `thumbnail_face_id` string, nullable — ID of the face resource used as this person's thumbnail
    - `asset_urls` object, nullable — Asset variants from this person's thumbnail face. May be null when embedded in an AssetResponse; use /api/people endpoints for full person data.
    - `cluster_metrics` ClusterMetricsResponse — Cohesion metrics for a Person's face cluster — surfaced via ``include=cluster_metrics`` on the people endpoints. These describe how tight the cluster is in embedding space (lower = more cohesive) and drive both the production face-assignment cohesion gate and the operator-facing face cleanup dashboard.
      - `pairwise_p90` number, required — 90th-percentile pairwise cosine distance between faces in this person's cluster. Lower = more cohesive cluster; loose clusters (higher pairwise_p90) are gated out of the face-assignment path to prevent further drift.
      - `pairwise_mean` number, required — Mean pairwise cosine distance between faces in this person's cluster.
      - `face_count` integer, required — Number of faces that fed into the centroid and pairwise metrics. This is the cluster-membership count, **not** the same as `asset_count` — `face_count` counts every face row, while `asset_count` counts distinct assets (one asset can contribute multiple faces of the same person).
    - `created_at` string, date-time, required — When this person record was created
    - `updated_at` string, date-time, required — When this person record was last updated
  - `kind` string, required — What produced the current rendering: `original` (the upload), `edit` (a client-baked edit), or `external:<service>`. The namespace is open — derive edited-ness as `kind != "original"`.
  - `current_version_id` string, required — ID (`asset_version_` prefix) of the current version, which the top-level rendering fields describe. Pass it as the expected current version on version writes so a racing write is rejected.
  - `width` integer — Width of the current rendering in pixels.
  - `height` integer — Height of the current rendering in pixels.
  - `duration` number, nullable — Video length in seconds. `null` for images and for videos whose duration has not been extracted yet.
  - `trashed_at` string, date-time, nullable — When this asset was moved to trash (ISO 8601, UTC). `null` for live assets. Trashed assets are excluded from default list/search results and are purged after the configured retention window.
  - `stack_id` string, nullable — ID of the stack this asset belongs to (`asset_stack_` prefix), or `null` when the asset is not part of a stack. Group assets by this value to collapse a stack into a single tile; the stack's own cover and member count are not carried on the asset. Distinct from `metadata.auto_stack_id`, which is the camera's in-EXIF `MakerNotes:AutoStackID` string — this is the server-assigned foreign key to the asset's stack.

## Other responses

- `201` — Successful Response
- `400` — No target library could be resolved: `library_id` was omitted but the account has multiple live libraries, so no default could be chosen. Retry with an explicit `library_id`.
- `401` — Missing, invalid, or expired credentials.
- `403` — The credentials are valid but not authorized for this operation — for example an API key whose action or library scope excludes it, or a credential type this operation does not accept.
- `404` — Not found
- `422` — The multipart body is invalid: a required field is missing, a datetime is malformed, the file part has no filename, or the filename is over-long or not an image or video type.
- `429` — Rate limit exceeded. Retry after the interval in the `Retry-After` header.
- `502` — Transient upstream storage error while persisting the file. Retryable: retry the upload after the interval in the `Retry-After` header. The response detail carries the stable `error_code` `"transient_storage_error"` plus `asset_id` and `part_number` keys (possibly null).
- `507` — The account or the target library is at its storage cap. Not retryable until space is freed or the cap is raised.

---

[API](https://skmtc.net/gumnut-ai/apis/gumnut-api.md) · [All operations](https://skmtc.net/gumnut-ai/apis/gumnut-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/gumnut-ai/gumnut-api/versions/e71db45f5d4a/schema)
