# POST /v1/projects/:projectId/media/finalize (/docs/api/reference/media/finalize-upload)



<Endpoint method="POST" path="/v1/projects/:projectId/media/finalize" scope="projects:write" phase="1" />

Promotes a completed upload into the project's media library, returning a stable `assetId` (a.k.a. `mediaId`) you can reference from influencers, content, and ad creative. Call this immediately after the signed `PUT` succeeds — unfinalized uploads are garbage-collected after 24 hours.

Finalize verifies the object exists (via HEAD), checks the declared size matches, and inserts the row. It is safe to call more than once with the same `uploadId` — the first call creates the row, subsequent calls return the same asset.

<Parameters
  title="Path"
  rows="[
  { name: 'projectId', type: 'string (uuid)', required: true, description: 'Project ID.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'uploadId', type: 'string', required: true, description: 'The `uploadId` returned by [`POST /media/presign`](/docs/api/reference/media/presign-upload).' },
  { name: 'checksumSha256', type: 'string (64 hex)', description: 'Optional sha256 to persist on the asset metadata.' },
]"
/>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl -X POST https://api.layers.com/v1/projects/{projectId}/media/finalize \
      -H "X-Api-Key: $LAYERS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "uploadId": "upl_01HXA3KMNP4RSTUVWXYZABCDEF" }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const res = await fetch(
      `https://api.layers.com/v1/projects/${projectId}/media/finalize`,
      {
        method: 'POST',
        headers: {
          'X-Api-Key': process.env.LAYERS_API_KEY!,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ uploadId }),
      },
    );
    const asset = await res.json();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, httpx

    r = httpx.post(
        f"https://api.layers.com/v1/projects/{project_id}/media/finalize",
        headers={
            "X-Api-Key": os.environ["LAYERS_API_KEY"],
            "Content-Type": "application/json",
        },
        json={"uploadId": upload_id},
    )
    asset = r.json()
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="Asset row created (or echoed if re-finalized).">
  ```json
  {
    "mediaId": "med_01HXA4MNP5RSTUVWXYZABCDEFGH",
    "assetId": "med_01HXA4MNP5RSTUVWXYZABCDEFGH",
    "url": "https://media.layers.com/.../founder-shot.jpg",
    "kind": "reference_image",
    "contentType": "image/jpeg",
    "mimeType": "image/jpeg",
    "byteSize": 482112,
    "sizeBytes": 482112,
    "filename": "founder-shot.jpg",
    "sha256": null,
    "checksumSha256": null,
    "createdAt": "2026-04-18T19:26:02Z"
  }
  ```
</Response>

## Notes [#notes]

* **`mediaId` and `assetId` are aliases.** Same uuid, two keys — use either. Newer code paths (influencer reference-images, content assets) accept both.
* **`contentType` and `mimeType` are aliases.** Same string, two keys.
* **`byteSize` and `sizeBytes` are aliases.** Same number.
* **Re-finalize is a no-op.** Calling finalize a second time with the same `uploadId` returns the existing row — use this for idempotent retries.

## Errors [#errors]

| Status | Code                | When                                                                                |
| ------ | ------------------- | ----------------------------------------------------------------------------------- |
| 401    | `UNAUTHENTICATED`   | Missing or invalid key.                                                             |
| 403    | `FORBIDDEN_SCOPE`   | Key lacks `projects:write`.                                                         |
| 404    | `NOT_FOUND`         | Project or `uploadId` does not exist.                                               |
| 409    | `UPLOAD_INCOMPLETE` | Upload object has not been PUT, or its size does not match the presign declaration. |
| 410    | `UPLOAD_EXPIRED`    | `uploadId` is older than 24 hours. Call presign again.                              |
| 422    | `VALIDATION_FAILED` | Malformed `uploadId`.                                                               |
| 429    | `RATE_LIMITED`      | Write budget exhausted.                                                             |

## See also [#see-also]

* [`POST /v1/projects/:projectId/media/presign`](/docs/api/reference/media/presign-upload) — step 1
* [`POST /v1/projects/:projectId/media`](/docs/api/reference/media/upload-media-inline) — inline upload (bypasses presign + finalize)
