## Upload an attachment

`POST /v1/feedback/attachments`

Uploads an image or a document and returns its ID for later linking to a ticket or comment. The upload is two-step: first the file is sent here, then the returned `id` is passed in the `attachmentIds` field when [creating a ticket](./submit.md) or [adding a comment](./comments.md).

The file is passed as `multipart/form-data` in the `file` field; its type is determined by the extension in the file name. PNG, JPEG, WebP, and GIF images are re-encoded on the server: PNG, JPEG, and WebP are converted to WebP, and GIF is re-encoded as GIF, preserving animation. TXT, MD, CSV, HTML, XLSX, and ZIP documents are stored unchanged. Limits: up to 10 MB per file, up to 24 million pixels per image, up to 5 attachments per message and up to 25 per ticket.

## Request fields (multipart/form-data)

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `file` | file | yes | A file up to 10 MB. An image — extension `.png`, `.jpg`, `.jpeg`, `.webp`, or `.gif`. A document — `.txt`, `.md`, `.csv`, `.html`, `.htm`, `.xlsx`, or `.zip` |

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/feedback/attachments \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "file=@screenshot.png"
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/feedback/attachments \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -F "file=@screenshot.png"
```

### JavaScript — personal key

```javascript
const form = new FormData()
form.append('file', fileInput.files[0])

const res = await fetch('https://vibecode.bitrix24.com/v1/feedback/attachments', {
  method: 'POST',
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
  body: form,
})
const { data } = await res.json()
// pass data.id in attachmentIds when creating a ticket or comment
```

### JavaScript — OAuth application

```javascript
const form = new FormData()
form.append('file', fileInput.files[0])

const res = await fetch('https://vibecode.bitrix24.com/v1/feedback/attachments', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
  body: form,
})
const { data } = await res.json()
```

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `true` on success |
| `data` | object | The uploaded attachment |
| `data.id` | string | Attachment UUID. Passed in `attachmentIds` when creating a ticket or comment |
| `data.kind` | string | Attachment class: `IMAGE` or `DOCUMENT` |
| `data.mime` | string | Type after processing. For an image — `image/webp` for PNG, JPEG, and WebP, `image/gif` for GIF. For a document — the type by extension, for example `text/plain` or `application/zip` |
| `data.sizeBytes` | number | Size of the stored file in bytes |
| `data.width` | number | Width in pixels. `0` for a document |
| `data.height` | number | Height in pixels. `0` for a document |
| `data.thumbnailUrl` | string · null | Path to the attachment thumbnail. `null` for a document |
| `data.originalName` | string · null | Original file name. `null` if the image was uploaded without a file name. A document without a name gets `attachment.<extension>` |
| `data.expiresAt` | string | The time after which an unlinked attachment is deleted (ISO 8601) |

## Response example

The image `screenshot.png` was uploaded:

```json
{
  "success": true,
  "data": {
    "id": "15ae6203-e67c-4378-912c-9b0465360c79",
    "kind": "IMAGE",
    "mime": "image/webp",
    "sizeBytes": 560,
    "width": 640,
    "height": 360,
    "thumbnailUrl": "/v1/feedback/_orphan/15ae6203-e67c-4378-912c-9b0465360c79/thumb",
    "originalName": "screenshot.png",
    "expiresAt": "2026-10-09T09:27:47.506Z"
  }
}
```

The document `note.txt` was uploaded — no thumbnail and no dimensions:

```json
{
  "success": true,
  "data": {
    "id": "f4ec1f18-431b-49ec-85bb-bedcfce6da7d",
    "kind": "DOCUMENT",
    "mime": "text/plain",
    "sizeBytes": 16,
    "width": 0,
    "height": 0,
    "thumbnailUrl": null,
    "originalName": "note.txt",
    "expiresAt": "2026-10-09T09:25:40.292Z"
  }
}
```

## Error response example

413 — file larger than 10 MB:

```json
{
  "success": false,
  "error": {
    "code": "IMAGE_TOO_LARGE",
    "message": "File too large: a feedback attachment is capped at 10 MB (10485760 bytes)",
    "hint": "Shrink the image below 10 MB (lower the resolution or re-encode as JPEG or WebP) and upload it again. The cap applies to each file and is not raised by retrying."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|------|-------------|
| 400 | `NO_FILE` | No file was passed in the `file` field |
| 400 | `INVALID_MIME` | The file name extension is not on the allowed list. A file without an extension is rejected unless its `Content-Type` is `image/png`, `image/jpeg`, `image/webp`, or `image/gif` |
| 400 | `MIME_MISMATCH` | The contents do not match the type: an image signature is not recognized as PNG, JPEG, WebP, or GIF, or a `.zip` or `.xlsx` file lacks the ZIP archive signature. A mismatch between two image formats is not an error — processing follows the actual contents |
| 400 | `INVALID_FILE_EMPTY` | The file is empty |
| 400 | `IMAGE_DECODE_FAILED` | The file could not be parsed as an image: the data is corrupted, the image has no dimensions, or processing did not finish within 5 seconds |
| 400 | `IMAGE_TOO_MANY_PIXELS` | More than 24 million pixels |
| 400 | `TOO_MANY_FRAMES` | The animation has more than 30 frames |
| 402 | `ACCOUNT_FROZEN` | The balance is frozen — uploading attachments is unavailable while it is frozen |
| 403 | `MANAGEMENT_KEY_READ_ONLY` | A management key cannot upload attachments |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is read-only — see [access rights](/docs/access-rights) |
| 413 | `IMAGE_TOO_LARGE` | File larger than 10 MB. The same code applies to images and documents |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header was not passed |
| 429 | `RATE_LIMITED` | More than 5 uploads per minute from one key. This is the platform-wide cap; the current limit is returned in the `X-RateLimit-Limit` header |

For the full list of common API errors, see [Errors](/docs/errors).

## Known specifics

**An attachment lives for an hour before it is linked.** An uploaded attachment is initially not linked to any ticket and is deleted after `expiresAt` (in one hour) if its `id` is not passed in `attachmentIds` when creating a ticket or comment. Once linked, the attachment is stored together with the ticket.

**Format after processing.** PNG, JPEG, and WebP are converted to WebP, so their `mime` in the response is `image/webp`. GIF is re-encoded as GIF (`image/gif`), preserving animation, up to 30 frames; beyond that the upload is rejected with `400 TOO_MANY_FRAMES`. The `sizeBytes`, `width`, and `height` fields refer to the processed file. A document is stored byte for byte; its `mime` is determined by the extension, not the contents. An archive is never unpacked — for `.zip` and `.xlsx` only the ZIP signature is checked.

**The attachment class is chosen by the file name extension, not by `Content-Type`.** An image is processed by its actual contents, so a JPEG saved as `image.png` is accepted, and the `mime` in the response is the result of re-encoding. A file without an extension — for example `blob`, the default name `FormData.append` assigns to a `Blob` object — is accepted only as an image and only when its `Content-Type` is `image/png`, `image/jpeg`, `image/webp`, or `image/gif`.

## Operations on a linked attachment

Once linked to a ticket, an attachment is available for download and deletion by its `id` (the `data.attachments[].id` field in [Get a ticket](./get.md)). These operations work with binary data, so they have no JSON body and no standard set of 4 examples.

### Download an attachment

`GET /v1/feedback/:id/attachments/:attId/file`

Returns the attachment file. Available to a key that has access to the ticket. An image arrives with its own type — `image/webp` or `image/gif` — and supports `ETag` and `If-None-Match`: a conditional request returns `304 Not Modified`. A document always arrives as `application/octet-stream` with the `Content-Disposition: attachment` header and the original file name, without `ETag`.

| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `id` (path) | string | yes | Ticket UUID |
| `attId` (path) | string | yes | Attachment UUID from `data.attachments[].id` |

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/feedback/a1b2c3d4-1111-2222-3333-444455556666/attachments/1209c405-1f99-4240-95e5-d5cc0cd43560/file" \
  -o attachment.webp
```

If the attachment does not exist, is not accessible to the key, or does not belong to the specified ticket, the request returns `404 NOT_FOUND`. If the balance is frozen, it returns `402 ACCOUNT_FROZEN`.

### Attachment thumbnail

`GET /v1/feedback/:id/attachments/:attId/thumb`

Returns a scaled-down thumbnail of an image. `Content-Type` is always `image/jpeg`. Parameters and access rules are the same as for the file download. A document has no thumbnail — the request returns `404 NOT_FOUND`.

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/feedback/a1b2c3d4-1111-2222-3333-444455556666/attachments/1209c405-1f99-4240-95e5-d5cc0cd43560/thumb" \
  -o thumb.jpg
```

### Delete an attachment

`DELETE /v1/feedback/:id/attachments/:attId`

Deletes the attachment. Available to whoever uploaded it. On success, returns `204 No Content` with an empty body. Deletion is not allowed if the other side has already replied after the attachment was uploaded.

| Parameter | Type | Required | Description |
|-----------|------|:--------:|-------------|
| `id` (path) | string | yes | Ticket UUID |
| `attId` (path) | string | yes | Attachment UUID |
| `reason` (body) | string | no | Deletion reason, 3–500 characters |

```bash
curl -X DELETE -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/feedback/a1b2c3d4-1111-2222-3333-444455556666/attachments/1209c405-1f99-4240-95e5-d5cc0cd43560"
```

| HTTP | Code | Description |
|------|------|-------------|
| 402 | `ACCOUNT_FROZEN` | The balance is frozen |
| 403 | `DELETE_NOT_ALLOWED` | The other side has already replied after the attachment was uploaded |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is read-only — see [access rights](/docs/access-rights) |
| 400 | `REASON_INVALID` | `reason` shorter than 3 or longer than 500 characters |
| 404 | `NOT_FOUND` | The attachment was not found, is not owned by the key, or does not belong to the ticket |

## See also

- [Submit a ticket](/docs/feedback/submit)
- [Add a comment](/docs/feedback/comments)
- [Feedback](/docs/feedback)
- [Errors](/docs/errors)
