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



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

Returns a short-lived signed URL you can `PUT` media to directly, without routing bytes through the Layers API. Use it for any file over 256KB — logos, reference images, uploaded video, audio. For smaller files, [`POST /v1/projects/:projectId/media`](/docs/api/reference/media/upload-media-inline) takes base64 inline.

The signed URL points at Layers' managed object storage. After the `PUT` succeeds, call [`POST /v1/projects/:projectId/media/finalize`](/docs/api/reference/media/finalize-upload) with the `uploadId` to register the file in the media library — without finalize, the upload is orphaned and garbage-collected after 24 hours.

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

<Parameters
  title="Body"
  rows="[
  { name: 'kind', type: 'string', required: true, description: 'Media class — determines allowed content types and downstream handling.', enum: ['image', 'video', 'audio', 'logo', 'reference_image'] },
  { name: 'filename', type: 'string', description: 'Original filename (1-256 chars). Optional — when provided, the storage key uses its extension.' },
  { name: 'mimeType', type: 'string', required: true, description: 'MIME type (e.g. `image/png`, `video/mp4`). Must match what your PUT sends. `contentType` also accepted for backwards compat.' },
  { name: 'sizeBytes', type: 'integer', description: 'Declared file size in bytes. Optional — when provided, finalize verifies the PUT body matches this and presign returns the matching `Content-Length` header. `byteSize` also accepted.' },
  { name: 'checksumSha256', type: 'string (64 hex)', description: 'Optional sha256 to record 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/presign \
      -H "X-Api-Key: $LAYERS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "reference_image",
        "filename": "founder-shot.jpg",
        "mimeType": "image/jpeg",
        "sizeBytes": 482112
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    const res = await fetch(
      `https://api.layers.com/v1/projects/${projectId}/media/presign`,
      {
        method: 'POST',
        headers: {
          'X-Api-Key': process.env.LAYERS_API_KEY!,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          kind: 'reference_image',
          filename: file.name,
          mimeType: file.type,
          sizeBytes: file.size,
        }),
      },
    );
    const { uploadId, url, headers } = await res.json();

    await fetch(url, { method: 'PUT', body: file, headers });

    const finalize = 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 { assetId } = await finalize.json();
    ```
  </Tab>

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

    presign = httpx.post(
        f"https://api.layers.com/v1/projects/{project_id}/media/presign",
        headers={
            "X-Api-Key": os.environ["LAYERS_API_KEY"],
            "Content-Type": "application/json",
        },
        json={
            "kind": "reference_image",
            "filename": "founder-shot.jpg",
            "mimeType": "image/jpeg",
            "sizeBytes": len(body),
        },
    ).json()

    httpx.put(presign["url"], content=body, headers=presign["headers"])

    finalize = 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": presign["uploadId"]},
    ).json()
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="200" description="Presigned upload handle. `url` and `signedUrl` are aliases — use either.">
  ```json
  {
    "uploadId": "upl_01HXA3KMNP4RSTUVWXYZABCDEF",
    "url": "https://r2.layers.com/uploads/.../upl_01HXA3.../founder-shot.jpg?X-Amz-...",
    "signedUrl": "https://r2.layers.com/uploads/.../upl_01HXA3.../founder-shot.jpg?X-Amz-...",
    "method": "PUT",
    "headers": {
      "Content-Type": "image/jpeg",
      "Content-Length": "482112"
    },
    "expiresAt": "2026-04-18T19:40:42Z"
  }
  ```
</Response>

## Notes [#notes]

* **`url` and `signedUrl` are the same value.** Both keys are returned so older SDKs (which expected `signedUrl`) continue to work. Prefer `url` going forward.
* **`expiresAt` is the signed URL expiry (\~15 min).** The `uploadId` itself survives 24 hours until garbage collection, but the PUT must happen within the signed-URL window.
* **Size caps are per-kind.** `image`/`logo`/`reference_image`: 50 MB. `audio`: 200 MB. `video`: 1 GB. Exceeding the cap is a `413 PAYLOAD_TOO_LARGE`.

## Errors [#errors]

| Status | Code                | When                                                      |
| ------ | ------------------- | --------------------------------------------------------- |
| 401    | `UNAUTHENTICATED`   | Missing or invalid key.                                   |
| 403    | `FORBIDDEN_SCOPE`   | Key lacks `projects:write`.                               |
| 404    | `NOT_FOUND`         | Project does not exist.                                   |
| 413    | `PAYLOAD_TOO_LARGE` | `sizeBytes` exceeds the per-kind limit.                   |
| 422    | `VALIDATION_FAILED` | Unknown `kind`, missing `mimeType`, bad `sizeBytes`, etc. |
| 429    | `RATE_LIMITED`      | Write budget exhausted.                                   |

## See also [#see-also]

* [`POST /v1/projects/:projectId/media/finalize`](/docs/api/reference/media/finalize-upload) — register the uploaded file
* [`POST /v1/projects/:projectId/media`](/docs/api/reference/media/upload-media-inline) — inline upload for files under 256KB
