---
title: "Read an image's metadata synchronously"
method: POST
path: "/api/v1/images/metadata"
tags: ["Synchronous images"]
---

# Read an image's metadata synchronously

`POST /api/v1/images/metadata`

## Request

- Base URL: `https://api.imagestep.dev`
- URL: `https://api.imagestep.dev/api/v1/images/metadata`
- Auth: API key in header `Authorization`

EXIF, GPS, dimensions, format and SHA-1 for the image in the request body.
Nothing is stored, no credits are spent, and it does NOT count against the
plan's processing allowance — it reads a header.

The image is sent the ways `/images/transform` takes it: raw bytes with their own
`Content-Type`, a multipart `file` part, or JSON `{assetId}` / `{url}`.

Returns the same `image` / `metadata` shape `GET /api/v1/assets/{id}`
carries, so measuring an image before storing it and reading one after storing
it hand back the same fields.

LIMITS: the transform lane's — 25 MB, 50 megapixels, 10 s — and its 4 calls in flight per account (`429 rate_limited`, `Retry-After: 1`).

USE THIS WHEN:
- You want to know what you are holding before deciding whether to store it

DO NOT USE WHEN:
- The image is already an asset → GET /api/v1/assets/{id} already has this

## Request body

- SyncReferenceInput: The JSON form of a synchronous call: a reference to the image instead of its bytes. Give `assetId` or `url`. On `/images/transform` the op, preset and parameters may ride here too; a query parameter of the same name wins.
  - `op` string: Transform only: the op, as `?op=`
  - `preset` string: Transform only: the preset, as `?preset=`
  - `assetId` string: One of your assets, read by this service (its browser-readable rendition when it has one)
  - `url` string: An image on the internet, fetched by this service: `http(s)` only, never a private or loopback address, redirects not followed. Its format is the type it was served as, else the one its extension names
  - `parameters` object: Transform with `op` only: the op's parameters. A key also in the query string takes the query's value.

## Response `200`

`data` is `{image, metadata, durationMs}`: `image` has the fields of an asset's `image` (mime type, dimensions, size, SHA-1 and what is derived from the pixels), `metadata` the map exiftool read under its own tag names, `durationMs` the worker's time.

- ApiResponseImageMetadata: Standard API response wrapper containing status, message, data and timestamp
  - `success` boolean, required: Indicates if the request was successful
  - `message` string: Human-readable message about the response
  - `data` ImageMetadata, required: An image read without storing it: the facts an asset's `image` would have, and its metadata
    - `image` AssetImage, required: What an asset is, as ingest measured it: type, pixel size, bytes, SHA-1, and the facts a caller would otherwise download the image to learn. A derived fact is absent when it could not be derived
      - `mimeType` string: The type it is stored as, e.g. image/jpeg
      - `height` integer, required: Pixel height; 0 until ingest has measured it
      - `width` integer, required: Pixel width; 0 until ingest has measured it
      - `size` integer, required: Stored size in bytes; 0 until ingest has measured it
      - `sha1Hash` string: SHA-1 of the stored bytes, hex — the same bytes, not merely the same picture (that is phash)
      - `dominantColor` string: The dominant colour, #rrggbb
      - `blurhash` string: A BlurHash placeholder to paint while the image loads. It has no alpha channel: prefer thumbhash for an image with transparency
      - `thumbhash` string: A ThumbHash placeholder — smaller than blurhash, closer to the picture, and it keeps alpha
      - `phash` string: 64-bit perceptual hash, 16 hex digits: two images a few bits apart are the same picture, even re-encoded or resized. Meaningful for photographs, not flat graphics
      - `hasAlpha` boolean: Whether the file has an alpha channel (app stores refuse one)
      - `opaque` boolean: Whether every pixel is fully opaque — false only when the alpha channel is actually used
      - `animated` boolean: Whether it has more than one frame or page
      - `pages` integer: Frame or page count; 1 for a still image
      - `colorSpace` string: The colour space the file is in, e.g. srgb or cmyk (a CMYK original goes grey on the web)
      - `density` number, double: Dots per inch, when the file records it
      - `hasIccProfile` boolean: Whether the file embeds an ICC colour profile
    - `metadata` object, required: Everything the file says about itself, as exiftool read it, under exiftool's own tag names — the map an asset stores as `metadata`; every value is a string
    - `durationMs` integer: How long the worker took, in milliseconds
  - `timestamp` string, date-time: Timestamp when the response was generated
  - `error` ApiError: Structured error. Present on every 4xx/5xx response, absent on success.
    - `code` string: Machine-readable error code from a closed set
    - `message` string: Human-readable explanation. Never parse this — parse `code`.
    - `retryable` boolean, required: Whether repeating the same request unchanged could succeed. The whole reason this field exists is that an agent cannot infer it from the status code.
    - `param` string: Which request parameter the error is about, when it is about one
    - `details` object: Extra machine-readable context, e.g. per-field validation messages
    - `requestId` string: The id of this request, identical to the `X-Request-Id` response header. Quote it when reporting a failure: it is what finds the request in our logs.
  - `meta` PageMeta
    - `total` integer: Rows matching the filter. Page mode only: a cursor request counts nothing
    - `page` integer: The page served, from 0. Page mode only
    - `perPage` integer: Rows per page actually used (out of range is clamped)
    - `hasMore` boolean: Whether there is another page
    - `nextCursor` string, nullable: Pass back as `cursor` for the rows after this page; null on the last page. Opaque — send it unchanged, with the same filters

## Other responses

- `400`: - `unsupported_format` — not an image, a format this service does not read, or an input over 50 megapixels - `invalid_param` on `file` / `url` — a multipart body with no `file` part; a `url` this service will not fetch, or one that answers an HTTP error other than 5xx / 429 / 408 - `invalid_state` on `assetId` — the asset has no stored object yet
- `404`: `asset_not_found` — the JSON form's `assetId` is not one of your assets
- `413`: `payload_too_large` on `file` — the image is over 25 MB; `details.limit` is the ceiling in bytes
- `415`: `unsupported_media_type` — a form-encoded body; `details.supported` lists what is read
- `429`: Rate limited — `error.code` is `rate_limited` and `retryable` is true. `Retry-After` says how many seconds to wait.
- `503`: `provider_unavailable`, retryable, with `Retry-After` — `details.reason` is `capacity`, `deadline_exceeded` or `worker_unreachable`, `storage_read` for the JSON form's stored asset, or `url_unavailable` when the `url`'s host answered 5xx, 429 or 408, or not in time.

---

[API](https://skmtc.dev/imagestep/apis/imagestep-api.md) · [All operations](https://skmtc.dev/imagestep/apis/imagestep-api/llms.txt) · [OpenAPI document](https://skmtc.dev/imagestep/apis/imagestep-api/revisions/5e8e922a3085?raw)
