Attachments and Images (attachment.img)
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 accepts the file content directly on the
Attachment asset, Base64-encoded:
[
{ "@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, 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:
{
"AssetType": "Attachment",
"Asset": "Defect:1234",
"Content": "",
"ContentType": "text/plain",
"Filename": "sample.txt",
"Name": "Sample Text"
}
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:
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:
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.
query.v1 or api/assetSelecting 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
GET ~/attachment.img/{id}
HEAD ~/attachment.img/{id}
POST ~/attachment.img/{id}
{id} is the numeric asset ID, not the OID tokenattachment.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 |
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 as MaximumAttachmentSize (bytes), and still
handle 413.
Related
- Bulk API — creating the
Attachmentasset - Aliases — attaching to an asset created in the same request
- Attribute types —
Blobis the underlying type