---
title: "Update an asset"
method: PATCH
path: "/api/assets/{asset_id}"
tags: ["assets"]
---

# Update an asset

`PATCH /api/assets/{asset_id}`

Edits the user-editable metadata for a single asset — description, GPS coordinates, and original capture datetime. Only fields included in the request body are changed; others are left untouched. Passing ``null`` for a field removes a previously-set value; the response then falls back to the value embedded in the file when present. ``latitude`` and ``longitude`` must be set together (both written or both cleared).

Setting or clearing GPS coordinates re-enqueues reverse geocoding so location names refresh against the new effective coordinates.

For editing multiple assets in one round trip, prefer `bulk_update_assets`.

## Path parameters

- `asset_id` string, required — Asset ID (with `asset_` prefix) of the asset to update.

## Request body

- UpdateAssetRequest — User-editable metadata for a single asset. Mirrors the field shape of ``UserAssetMetadataUpdate``; the service model owns cross-field validation. See ``photos-api/docs/references/api-design.md`` for the router-DTO + service-model pattern.
  - `description` string, nullable — User-set description for the asset. Pass ``null`` to remove a previously-set value (the response then falls back to the description embedded in the file, if any). Omit to leave unchanged. Distinct from the AI-generated `description` field on the response — this writes to `metadata.description`.
  - `latitude` number, nullable — GPS latitude in decimal degrees, ``[-90, 90]``. Must be set together with ``longitude``. Pass ``null`` (along with ``longitude=null``) to remove a previously-set value; omit to leave unchanged.
  - `longitude` number, nullable — GPS longitude in decimal degrees, ``[-180, 180]``. Must be set together with ``latitude``. Pass ``null`` (along with ``latitude=null``) to remove a previously-set value; omit to leave unchanged.
  - `original_datetime` string, date-time, nullable — When the asset was originally captured. Aware values store the offset from ``utcoffset()`` alongside; naive values store NULL offset. Pass ``null`` to remove a previously-set value — the response then falls back to the datetime embedded in the file when present, otherwise to the file's upload timestamp. Omit to leave unchanged.

## Response `200`

Successful Response

- 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

- `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` — Validation Error
- `429` — Rate limit exceeded. Retry after the interval in the `Retry-After` header.

---

[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)
