Skip to main content

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:

1. Create the asset
{
"AssetType": "Attachment",
"Asset": "Defect:1234",
"Content": "",
"ContentType": "text/plain",
"Filename": "sample.txt",
"Name": "Sample Text"
}
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:

AttributeDescription
AssetThe asset the attachment belongs to
NameDisplay name
FilenameThe file name
ContentTypeMIME type
ContentBase64 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.

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​

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.

MethodBehaviour
GETDownload the blob. Returns an ETag; a matching If-None-Match gets a cached response
HEADAs GET, without the body
POSTUpload, replacing the blob. Responds text/plain
anything else405, 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​

StatusBodyCause
400Invalid UrlNo id in the path — attachment.img with nothing after it
400Invalid IDThe path segment is not a plain integer
401Unauthenticated. Every blob endpoint requires authentication
404Not FoundNo blob for that id
413Maximum Content Length ExceededUpload larger than the server's maxRequestLength
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:

EndpointHolds
attachment.imgAttachment files
image.imgImages such as member avatars
embedded.imgImages embedded in rich-text fields
expressionimage.imgImages posted in conversations
downloadblob.imgDownload 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.

  • Bulk API — creating the Attachment asset
  • Aliases — attaching to an asset created in the same request
  • Attribute types — Blob is the underlying type