---
id: attachments
slug: /apis/attachments
title: 'Attachments and Images (attachment.img)'
description: 'Upload and download attachment and image binaries. Two workflows, a family of blob endpoints, and an id format that is not an OID token.'
sidebar_label: 'Attachments (attachment.img)'
sidebar_position: 14
tags: ['api', 'attachments', 'images', 'binary', 'upload']
---

Binary content — attachment files, member avatars, images embedded in rich text —
does not travel through the asset APIs the way ordinary attribute values do. It
has its own family of endpoints, of which `attachment.img` is the one most people
need.

An attachment is **two things**: an `Attachment` asset holding the metadata, and a
blob holding the bytes. You can create both in one request or in two, and the
choice matters more than it looks.

## Two Ways to Create an Attachment

### One Request — Inline Base64

The [Bulk API](/apis/asset-bulk) accepts the file content directly on the
`Attachment` asset, Base64-encoded:

```json
[
  { "@newDefect": { "AssetType": "Defect", "Scope": "Scope:0", "Name": "An Example Defect" } },
  {
    "AssetType": "Attachment",
    "Asset": "@newDefect",
    "Content": "VGhpcyBpcyB0aGUgY29udGVudCBvZiB0aGUgdGV4dCBmaWxl",
    "ContentType": "text/plain",
    "Filename": "sample.txt",
    "Name": "Sample Text"
  }
]
```

Note the attachment references the Defect by [alias](/apis/asset-bulk/aliases-and-projection),
so both are created in a single call.

This is the simpler path, and it is bounded by request size — Base64 inflates the
payload by roughly a third, and the whole request is still subject to the
server's maximum request length.

### Two Requests — Create, Then Upload the Bytes

Create the `Attachment` asset with **empty** content, then `POST` the raw bytes to
`attachment.img`:

```json title="1. Create the asset"
{
  "AssetType": "Attachment",
  "Asset": "Defect:1234",
  "Content": "",
  "ContentType": "text/plain",
  "Filename": "sample.txt",
  "Name": "Sample Text"
}
```

```http title="2. Upload the bytes"
POST ~/attachment.img/5678
Content-Type: application/octet-stream

<raw bytes>
```

This is the path to use for anything large, and it is what the SDKs do
internally.

### `Attachment` Asset Attributes

All five are **required** on create:

| Attribute | Description |
| --- | --- |
| `Asset` | The asset the attachment belongs to |
| `Name` | Display name |
| `Filename` | The file name |
| `ContentType` | MIME type |
| `Content` | Base64 content, or empty string when uploading separately |

A create missing any of them fails with a violation naming one missing
attribute — `Violation'Required'AttributeDefinition'Content'Attachment` — and
the violations surface **one at a time**, in the order `Asset`, `Content`,
`ContentType`, `Filename`, `Name`, so fixing the named attribute can reveal
the next. `Content` must be *present* but may be the empty string (the
two-request flow depends on exactly that).

## Downloading Attachment Content

To read the bytes back, `GET` the blob endpoint by numeric id:

```http
GET ~/attachment.img/5678
```

It streams the content, sets a MIME `Content-Type`, and returns an `ETag`, so a
follow-up request with `If-None-Match` is served from cache. This is the path
the SDKs and the product UI use, and the one to hand to integrators.

If you would rather stay on the data API, `rest-1.v1` can return the content as
a Base64 attribute:

```http
GET ~/rest-1.v1/Data/Attachment/5678?sel=Content,ContentType,Filename
```

Reserve that for the occasional single attachment. The bytes come back
Base64-encoded inside the response body, which inflates them by about a third.

:::danger Do not bulk-download content through `query.v1` or `api/asset`

Selecting `Content` across many attachments — `query.v1` or `api/asset` with
`Content` in the selection — is the wrong tool, even though it "works". Every
blob is Base64-encoded inline in one response, so the server holds all of it in
memory at once, the payload balloons, and nothing can be HTTP-cached. A single
such request can pull tens of megabytes and pressure the whole instance.

Download blobs one at a time through `attachment.img/{id}` (cacheable, streamed),
and use the data APIs only to discover **which** attachments exist — project
`Asset`, `Filename`, `ContentType`, never `Content`, when listing.

:::

## The `attachment.img` Contract

```http
GET  ~/attachment.img/{id}
HEAD ~/attachment.img/{id}
POST ~/attachment.img/{id}
```

:::danger `{id}` is the numeric asset ID, not the OID token

`attachment.img/Attachment:5678` does **not** work. The handler parses the path
segment as a plain integer — digits only, no sign — so you must strip the type
prefix and pass `5678`.

This is easy to get wrong because every other API surface addresses assets by OID
token. If you are extracting the id from a Bulk API `assetsCreated.oidTokens`
entry, take the part **after** the colon.

:::

| Method | Behaviour |
| --- | --- |
| `GET` | Download the blob. Returns an `ETag`; a matching `If-None-Match` gets a cached response |
| `HEAD` | As `GET`, without the body |
| `POST` | Upload, replacing the blob. Responds `text/plain` |
| anything else | `405`, with `Allow: GET,HEAD,POST` |

**The upload body can be either form.** The handler takes the first uploaded file
if the request is a multipart form, and otherwise reads the raw request body. A
plain `POST` of the bytes works; so does a file upload.

Add `?deleted=1` to operate on an attachment whose asset is deleted.

### Errors

| Status | Body | Cause |
| --- | --- | --- |
| `400` | `Invalid Url` | No id in the path — `attachment.img` with nothing after it |
| `400` | `Invalid ID` | The path segment is not a plain integer |
| `401` | | Unauthenticated. Every blob endpoint requires authentication |
| `404` | `Not Found` | No blob for that id |
| `413` | `Maximum Content Length Exceeded` | Upload larger than the server's `maxRequestLength` |

:::note A missing blob answers 404 even for a disallowed method

The handler checks that the blob exists **before** it checks the HTTP method, so
a `DELETE` against a nonexistent id returns `404`, not `405`. You only see `405`
when the id resolves to something real.

Worth knowing when probing: a `404` does not confirm your method was acceptable.

:::

## The Rest of the Blob Family

`attachment.img` has siblings that behave the same way and differ only in what
they address:

| Endpoint | Holds |
| --- | --- |
| `attachment.img` | Attachment files |
| `image.img` | Images such as member avatars |
| `embedded.img` | Images embedded in rich-text fields |
| `expressionimage.img` | Images posted in conversations |
| `downloadblob.img` | Download only — `GET` |

Each also has an **OAuth variant** at `<name>.oauth.img` — `attachment.oauth.img`,
`image.oauth.img`, `embedded.oauth.img`, `expressionimage.oauth.img` — which
accept delegated authentication. Use the plain form with Basic authentication or
an access token, and the `.oauth.` form when presenting an OAuth token.

## Size Limits Are a Server Setting

The maximum upload is the ASP.NET `maxRequestLength` for the instance, not an API
constant. It differs between deployments, and an on-premise administrator can
change it. Do not hardcode a limit — read it from
[`config.v1`](/apis/config-v1) as `MaximumAttachmentSize` (bytes), and still
handle `413`.


## Related

- [Bulk API](/apis/asset-bulk) — creating the `Attachment` asset
- [Aliases](/apis/asset-bulk/aliases-and-projection) — attaching to an asset created in the same request
- [Attribute types](/reference/attribute-types) — `Blob` is the underlying type
