POST /v1/projects/:projectId/media/finalize
Register an object-storage upload into the project's media library.
POST
/v1/projects/:projectId/media/finalizePhase 1stable
- Auth
- Bearer
- Scope
- projects:write
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.
Path
projectIdstring (uuid)requiredProject ID.
Body
uploadIdstringrequiredThe `uploadId` returned by [`POST /media/presign`](/docs/api/reference/media/presign-upload).checksumSha256string (64 hex)optionalOptional sha256 to persist on the asset metadata.
Example request
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" }'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();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()Response
200Asset row created (or echoed if re-finalized).
{
"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"
}Notes
mediaIdandassetIdare aliases. Same uuid, two keys — use either. Newer code paths (influencer reference-images, content assets) accept both.contentTypeandmimeTypeare aliases. Same string, two keys.byteSizeandsizeBytesare aliases. Same number.- Re-finalize is a no-op. Calling finalize a second time with the same
uploadIdreturns the existing row — use this for idempotent retries.
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
POST /v1/projects/:projectId/media/presign— step 1POST /v1/projects/:projectId/media— inline upload (bypasses presign + finalize)