---
title: "Finalize an uploaded Asset"
method: POST
path: "/assets/{id}/finalize"
tags: ["Assets"]
---

# Finalize an uploaded Asset

`POST /assets/{id}/finalize`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/assets/{id}/finalize`
- Auth: HTTP bearer

Completes a reserved Asset from the object that actually landed, and promotes it to `ready`.

This is the only real validation point: a presigned `PUT` carries no policy document, so nothing could have stopped the client sending a different type — or far more bytes — than it declared. A refused upload is deleted rather than left to bill as an orphan.

Pass `advertiserId` to place the file in the same call. That starts the same Registration as `POST /assets/{id}/registrations`, and it starts one only — the Registration settles later. **The answer is the Asset either way.** A registration that could not start does not fail this call, because the file is stored and only the registration needs retrying — read `GET /assets/{id}` for the Registration and its state.

## Path parameters

- `id` string, required: The Asset the reservation made.

## Request body

- object
  - `advertiserId` string: Place the file in this advertiser’s library in the same call. Optional — send no body to store the file alone.

## Response `200`

The stored Asset, now `ready`. `sizeBytes` is read off the stored object. `mimeType` and `kind` are the declared type, verified against the stored bytes. `width`, `height` and `durationMs` are read from the header of the file. It carries no Registration, whether or not you asked for one.

- object
  - `contentHash` string, required: AdCrunch’s own hash of the stored bytes. A hint for de-duplication, never an identity, and never a provider’s hash. Empty when the store reports none.
  - `createdAt` number, required: Milliseconds since the Unix epoch, UTC.
  - `createdBy` string, required: The user who reserved the upload.
  - `deletedAt` number, nullable, required: Always `null` here. A deleted Asset is not answered.
  - `durationMs` number, nullable, required: How long the video runs, in milliseconds, read from the header of the file at finalize. `null` for an image. For a video, `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.
  - `filename` string, required: The name the file was uploaded under. Never a lookup key.
  - `height` number, nullable, required: The height in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.
  - `id` string, required: The id of the Asset, `ast_…`.
  - `key` string, required: Where the bytes are held. Read-only, and never sent by you.
  - `kind` 'image' | 'video', required: Follows from `mimeType`, so the stored bytes verify it at finalize. It selects how a provider takes the file.
  - `mimeType` string, required: The `Content-Type` the file was uploaded with, verified against the stored bytes at finalize.
  - `name` string, nullable, required: The name a person gave this Asset, and what a provider receives at a future registration. `null` until somebody renames it, and the filename is the fallback.
  - `organizationId` string, required: The organization that owns the Asset.
  - `sizeBytes` number, required: Read from the stored object at finalize.
  - `status` 'pending' | 'ready', required: `ready` once the bytes have been inspected. A `pending` Asset is a reservation whose bytes have not arrived, and no operation here answers one.
  - `updatedAt` number, nullable, required: When the Asset last changed, in milliseconds since the Unix epoch, UTC: at finalize, or at the last rename.
  - `width` number, nullable, required: The width in pixels as the file is displayed, with its rotation applied, read from the header of the file at finalize. `null` when the file does not state it, or for an Asset uploaded before AdCrunch read it.

## Other responses

- `400`: The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed.
- `401`: No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`.
- `402`: The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`.
- `403`: The caller does not hold `asset:write`. `error` is `forbidden`.
- `404`: No Asset with that id in this organization. A deleted one answers this too. `error` is `not_found`.
- `409`: The reservation exists and no bytes have arrived. `error` is `not_uploaded`.
- `413`: The **stored** file is over the limit for its kind, whatever was declared at reservation. `error` is `too_large`, and the object has been discarded.
- `415`: AdCrunch refused the **stored** file and deleted it. `error` names why: - `unsupported_type`: the file was uploaded with a `Content-Type` that AdCrunch does not accept. - `type_mismatch`: the bytes are an accepted type, but not the `Content-Type` the file was uploaded with. PUT it again with its real type. - `unreadable`: the bytes are no accepted type, or the file ends before its header does.

---

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