## Create presigned URL

`POST /v1/storage/objects`

The first step of uploading a file from 10 MB to 5 GB — returns a temporary URL the client uses to send the file directly via PUT; after sending, you call upload completion.

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|---------|
| `key` | string | yes | — | Logical object key: from 1 to 1024 characters, allowed `a-z`, `A-Z`, `0-9`, `.`, `_`, `/`, `-`; must not start with `/` or `.`, must not contain `..` |
| `contentType` | string | yes | — | File MIME type; must match the `Content-Type` header on the PUT request to `uploadUrl` |
| `visibility` | string | no | `PRIVATE` | Object visibility: `PRIVATE` or `PUBLIC` |
| `sizeBytes` | number | no | — | Expected file size in bytes; an optional hint, the actual size is captured at the completion step |
| `ttlSeconds` | number | no | `3600` | Presigned URL lifetime in seconds: from 60 to 86400 |

## Examples

### curl — personal key

```bash
# Step 1 — get a presigned URL
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "users/42/report.pdf",
    "contentType": "application/pdf",
    "visibility": "PRIVATE"
  }'

# Step 2 — send the file to the received uploadUrl
curl -X PUT "https://storage.example.com/upload/..." \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf
```

### curl — OAuth application

```bash
# Step 1 — get a presigned URL
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "users/42/report.pdf",
    "contentType": "application/pdf",
    "visibility": "PRIVATE"
  }'

# Step 2 — send the file to the received uploadUrl
curl -X PUT "https://storage.example.com/upload/..." \
  -H "Content-Type: application/pdf" \
  --data-binary @report.pdf
```

### JavaScript — personal key

```javascript
// Step 1 — get a presigned URL
const res = await fetch('https://vibecode.bitrix24.com/v1/storage/objects', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'users/42/report.pdf',
    contentType: 'application/pdf',
    visibility: 'PRIVATE',
  }),
})
if (!res.ok) {
  const { error } = await res.json()
  console.error(error.code, error.message)
  return
}
const result = await res.json()

// Step 2 — send the file to the presigned URL
const put = await fetch(result.uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: file, // File or Blob
})
if (!put.ok) throw new Error(`PUT failed: ${put.status}`)
```

### JavaScript — OAuth application

```javascript
// Step 1 — get a presigned URL
const res = await fetch('https://vibecode.bitrix24.com/v1/storage/objects', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'users/42/report.pdf',
    contentType: 'application/pdf',
    visibility: 'PRIVATE',
  }),
})
const result = await res.json()

// Step 2 — send the file to the presigned URL
await fetch(result.uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/pdf' },
  body: file, // File or Blob
})
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `objectId` | string | Object identifier; pass it to [`POST /v1/storage/objects/complete`](/docs/storage/upload/complete) |
| `uploadUrl` | string | Presigned URL for sending the file via the PUT method |
| `bucket` | string | Internal container name |
| `objectKey` | string | Internal object key in storage |
| `expiresAt` | string | Presigned URL expiry (ISO 8601) |

## Response example

```json
{
  "objectId": "cmpf9b1gp003omr0zkc0qjojn",
  "uploadUrl": "https://storage.example.com/upload/portals/example-portal/users/42/report.pdf?X-Amz-Signature=abc123",
  "bucket": "example-storage-bucket",
  "objectKey": "portals/example-portal/users/42/report.pdf",
  "expiresAt": "2026-05-21T09:57:43.675Z"
}
```

## Error response example

400 — `ttlSeconds` is outside the allowed range:

```json
{
  "success": false,
  "error": {
    "code": "STORAGE_INVALID_TTL",
    "message": "ttlSeconds must be between 60 and 86400"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 403 | `STORAGE_SCOPE_REQUIRED` | The API key lacks the `vibe:storage` scope |
| 401 | `STORAGE_NO_AUTH_CONTEXT` | The request was made without authorization |
| 400 | `STORAGE_KEY_REQUIRED` | The `key` field was not provided |
| 400 | `STORAGE_CONTENT_TYPE_REQUIRED` | The `contentType` field was not provided |
| 400 | `STORAGE_INVALID_KEY` | The `key` value violates the format rules |
| 400 | `STORAGE_INVALID_VISIBILITY` | Invalid `visibility` value |
| 400 | `STORAGE_INVALID_SIZE` | Invalid `sizeBytes` value |
| 400 | `STORAGE_INVALID_TTL` | `ttlSeconds` is outside the range 60–86400 |
| 402 | `BILLING_INSUFFICIENT` | Insufficient balance |
| 503 | `STORAGE_FEATURE_DISABLED` | Storage is disabled for the portal |
| 503 | `STORAGE_STS_UNAVAILABLE` | The temporary credentials issuance service is unavailable |
| 502 | `STORAGE_BUCKET_ERROR` | Error while interacting with storage |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

**Mandatory second step.** After sending the file to `uploadUrl` you must call [upload completion](/docs/storage/upload/complete) with the `objectId` from this response. Until completion, the object stays in `PENDING` status and cannot be downloaded.

**URL lifetime.** `ttlSeconds` limits the lifetime of the presigned upload URL, not the retention period of the finished object.

## See also

- [Upload](/docs/storage/upload)
- [Direct upload](/docs/storage/upload/direct)
- [Complete upload](/docs/storage/upload/complete)
- [Multipart upload](/docs/storage/upload/multipart-create)
- [Storage](/docs/storage)
