> For the complete documentation index, see [llms.txt](https://hub.equipme.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hub.equipme.io/development/api-reference-app/attachment.md).

# Attachment

An **attachment** is a file that belongs to a thread — uploaded first, then referenced when a message is posted. Uploads and messages are separate steps so that a file can be transferred while a message is still being written.

An attachment that is uploaded while composing carries `composing: true` and belongs to no message yet; posting a message with its id is what binds the two together.

## List the attachments of a thread

> Returns the attachments of a thread. \`threadId\` is required — attachments are never listed across\
> threads.\
> \
> \*\*Filterable\*\* — \`id\`, \`threadId\`, \`messageId\`, \`creatorId\`, \`name\`, \`contentType\`, \`size\`,\
> \`inline\`, \`composing\`, \`createdAt\`.\
> \
> \*\*Sortable\*\* — \`id\`, \`name\`, \`size\`, \`createdAt\`.\
> \
> \*\*Searchable\*\* — nothing yet; use \`filter\` with \`contains\` on \`name\`.<br>

```json
{"openapi":"3.1.0","info":{"title":"Solvio REST API","version":"1.0.0-beta"},"tags":[{"description":"An **attachment** is a file that belongs to a thread — uploaded first, then referenced when a message\nis posted. Uploads and messages are separate steps so that a file can be transferred while a message\nis still being written.\n\nAn attachment that is uploaded while composing carries `composing: true` and belongs to no message\nyet; posting a message with its id is what binds the two together.\n","name":"attachment"}],"servers":[{"description":"The host serving this document","url":"/api"}],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"description":"Token issued by the central equipme authentication, sent as `Bearer <token>`.","scheme":"bearer","type":"http"},"ApiKeyAuth":{"description":"Static key for machine-to-machine access. Alternative to BearerAuth.","in":"header","name":"X-API-Key","type":"apiKey"}},"schemas":{"v1.ListAttachmentsResponse-domain_Attachment":{"properties":{"count":{"type":"integer"},"data":{"items":{"$ref":"#/components/schemas/domain.Attachment"},"type":"array","uniqueItems":false}},"type":"object"},"domain.Attachment":{"properties":{"composing":{"description":"Composing marks the attachment as belonging to an active message composition session.\nThe term mirrors the \"compose\" state in messaging clients: the user is writing a message but has not sent it yet.\nOnce the message is sent, ClaimAttachments links these attachments to it and clears this flag.\nComposing attachments are excluded from all list queries and are eligible for periodic cleanup if never claimed.","type":"boolean"},"contentId":{"description":"ContentID is the MIME Content-ID value used in cid: URI references within the message body.","type":"string"},"contentType":{"type":"string"},"createdAt":{"type":"string"},"creator":{"$ref":"#/components/schemas/domain.Contact"},"creatorId":{"type":"integer"},"id":{"type":"integer"},"inline":{"description":"Inline indicates that the attachment is meant to be displayed within the message content rather than as a separate downloadable file.","type":"boolean"},"link":{"description":"Link refers to the download URL for the attachment. Relative path for the API endpoint that requires authorization,\nnot a pre-signed URL or direct S3 link. This is generated on the fly and not stored in the DB.","type":"string"},"messageId":{"type":"integer"},"name":{"type":"string"},"size":{"type":"number"},"threadId":{"description":"An attachment is listed under the thread or message it belongs to, so the two\nways back are redundant there and close both cycles in the schema. The ids\nidentify them; selecting the relations still works.","type":"integer"}},"type":"object"},"domain.Contact":{"properties":{"assignmentContext":{"type":"string"},"autoAssign":{"description":"AutoAssign / AssignmentContext drive automated ticket routing. Set today only on\nGROUP-kind contacts via the group endpoints; nil/empty on ordinary contacts.","type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"description":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"id":{"type":"integer"},"kind":{"$ref":"#/components/schemas/domain.ContactKind"},"lastName":{"type":"string"},"mobile":{"type":"string"},"phone":{"type":"string"},"tenant":{"$ref":"#/components/schemas/domain.Tenant"},"tenantId":{"type":"integer"},"title":{"type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"type":"object"},"domain.ContactKind":{"enum":["","PERSON","INTAKE","GROUP"],"type":"string"},"domain.Tenant":{"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"},"middleware.ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/middleware.ErrorBody"}},"type":"object"},"middleware.ErrorBody":{"properties":{"code":{"description":"Code is the stable, machine-readable reason. Clients match on this, not on the message.","type":"string"},"details":{"description":"Details carries structured context where an error has any — a conflict names what it\ncollided with. Absent otherwise."},"message":{"type":"string"}},"type":"object"}}},"paths":{"/v1/attachments":{"get":{"description":"Returns the attachments of a thread. `threadId` is required — attachments are never listed across\nthreads.\n\n**Filterable** — `id`, `threadId`, `messageId`, `creatorId`, `name`, `contentType`, `size`,\n`inline`, `composing`, `createdAt`.\n\n**Sortable** — `id`, `name`, `size`, `createdAt`.\n\n**Searchable** — nothing yet; use `filter` with `contains` on `name`.\n","operationId":"listAttachments","parameters":[{"description":"Thread id","in":"query","name":"threadId","required":true,"schema":{"type":"integer"}},{"description":"JSON filter expression","in":"query","name":"filter","schema":{"type":"string"}},{"description":"Free-text term — no effect here","in":"query","name":"search","schema":{"type":"string"}},{"description":"Comma-separated field:direction terms","in":"query","name":"sort","schema":{"type":"string"}},{"description":"Selection of fields and relations","in":"query","name":"fields","schema":{"type":"string"}},{"description":"Number of entries to skip","in":"query","name":"offset","schema":{"default":0,"type":"integer"}},{"description":"Page size, 1 to 100","in":"query","name":"limit","schema":{"default":100,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.ListAttachmentsResponse-domain_Attachment"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"List the attachments of a thread","tags":["attachment"]}}}}
```

## Upload attachments

> Uploads one or more files as \`multipart/form-data\` under the field name \`files\`.\
> \
> Either \`threadId\` names the thread the files belong to, or \`composing=true\` marks them as not yet\
> bound to a thread — in that case \`tenantId\` says where they live. Without one of the two the request\
> is answered with \`400\`.\
> \
> Because several files travel in one request, the answer distinguishes three outcomes:\
> \
> \| Status | Meaning |\
> \| --- | --- |\
> \| \`201\` | every file was stored; \`data\` holds them |\
> \| \`207\` | some were stored, some were not; \`errors\` names the failures per file |\
> \| \`422\` | none could be stored |\
> \
> An entry in \`errors\` carries the \`filename\` and what went wrong, so a client can retry exactly those.<br>

```json
{"openapi":"3.1.0","info":{"title":"Solvio REST API","version":"1.0.0-beta"},"tags":[{"description":"An **attachment** is a file that belongs to a thread — uploaded first, then referenced when a message\nis posted. Uploads and messages are separate steps so that a file can be transferred while a message\nis still being written.\n\nAn attachment that is uploaded while composing carries `composing: true` and belongs to no message\nyet; posting a message with its id is what binds the two together.\n","name":"attachment"}],"servers":[{"description":"The host serving this document","url":"/api"}],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"description":"Token issued by the central equipme authentication, sent as `Bearer <token>`.","scheme":"bearer","type":"http"},"ApiKeyAuth":{"description":"Static key for machine-to-machine access. Alternative to BearerAuth.","in":"header","name":"X-API-Key","type":"apiKey"}},"schemas":{"v1.UploadAttachmentsResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/domain.Attachment"},"type":"array","uniqueItems":false},"errors":{"items":{"$ref":"#/components/schemas/v1.UploadAttachmentsError"},"type":"array","uniqueItems":false}},"type":"object"},"domain.Attachment":{"properties":{"composing":{"description":"Composing marks the attachment as belonging to an active message composition session.\nThe term mirrors the \"compose\" state in messaging clients: the user is writing a message but has not sent it yet.\nOnce the message is sent, ClaimAttachments links these attachments to it and clears this flag.\nComposing attachments are excluded from all list queries and are eligible for periodic cleanup if never claimed.","type":"boolean"},"contentId":{"description":"ContentID is the MIME Content-ID value used in cid: URI references within the message body.","type":"string"},"contentType":{"type":"string"},"createdAt":{"type":"string"},"creator":{"$ref":"#/components/schemas/domain.Contact"},"creatorId":{"type":"integer"},"id":{"type":"integer"},"inline":{"description":"Inline indicates that the attachment is meant to be displayed within the message content rather than as a separate downloadable file.","type":"boolean"},"link":{"description":"Link refers to the download URL for the attachment. Relative path for the API endpoint that requires authorization,\nnot a pre-signed URL or direct S3 link. This is generated on the fly and not stored in the DB.","type":"string"},"messageId":{"type":"integer"},"name":{"type":"string"},"size":{"type":"number"},"threadId":{"description":"An attachment is listed under the thread or message it belongs to, so the two\nways back are redundant there and close both cycles in the schema. The ids\nidentify them; selecting the relations still works.","type":"integer"}},"type":"object"},"domain.Contact":{"properties":{"assignmentContext":{"type":"string"},"autoAssign":{"description":"AutoAssign / AssignmentContext drive automated ticket routing. Set today only on\nGROUP-kind contacts via the group endpoints; nil/empty on ordinary contacts.","type":"boolean"},"createdAt":{"format":"date-time","type":"string"},"description":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"id":{"type":"integer"},"kind":{"$ref":"#/components/schemas/domain.ContactKind"},"lastName":{"type":"string"},"mobile":{"type":"string"},"phone":{"type":"string"},"tenant":{"$ref":"#/components/schemas/domain.Tenant"},"tenantId":{"type":"integer"},"title":{"type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"type":"object"},"domain.ContactKind":{"enum":["","PERSON","INTAKE","GROUP"],"type":"string"},"domain.Tenant":{"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object"},"v1.UploadAttachmentsError":{"properties":{"error":{"type":"string"},"filename":{"type":"string"}},"type":"object"},"middleware.ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/middleware.ErrorBody"}},"type":"object"},"middleware.ErrorBody":{"properties":{"code":{"description":"Code is the stable, machine-readable reason. Clients match on this, not on the message.","type":"string"},"details":{"description":"Details carries structured context where an error has any — a conflict names what it\ncollided with. Absent otherwise."},"message":{"type":"string"}},"type":"object"}}},"paths":{"/v1/attachments":{"post":{"description":"Uploads one or more files as `multipart/form-data` under the field name `files`.\n\nEither `threadId` names the thread the files belong to, or `composing=true` marks them as not yet\nbound to a thread — in that case `tenantId` says where they live. Without one of the two the request\nis answered with `400`.\n\nBecause several files travel in one request, the answer distinguishes three outcomes:\n\n| Status | Meaning |\n| --- | --- |\n| `201` | every file was stored; `data` holds them |\n| `207` | some were stored, some were not; `errors` names the failures per file |\n| `422` | none could be stored |\n\nAn entry in `errors` carries the `filename` and what went wrong, so a client can retry exactly those.\n","operationId":"uploadAttachments","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"oneOf":[{"title":"files","type":"file"},{"title":"threadId","type":"integer"},{"title":"tenantId","type":"integer"},{"title":"composing","type":"boolean"}]}},"multipart/form-data":{"schema":{"type":"object"}}},"description":"Files to upload; repeat for several | Thread the files belong to | Tenant, required while composing | Upload without binding to a thread yet"},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UploadAttachmentsResponse"}}},"description":"Created"},"207":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/v1.UploadAttachmentsResponse"}}},"description":"Some files were stored, some were not"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Forbidden"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Upload attachments","tags":["attachment"]}}}}
```

## Delete an attachment

> Deletes an attachment and its stored file. A file that a message already refers to disappears from\
> that message. Answers with \`204\` and no body.<br>

```json
{"openapi":"3.1.0","info":{"title":"Solvio REST API","version":"1.0.0-beta"},"tags":[{"description":"An **attachment** is a file that belongs to a thread — uploaded first, then referenced when a message\nis posted. Uploads and messages are separate steps so that a file can be transferred while a message\nis still being written.\n\nAn attachment that is uploaded while composing carries `composing: true` and belongs to no message\nyet; posting a message with its id is what binds the two together.\n","name":"attachment"}],"servers":[{"description":"The host serving this document","url":"/api"}],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"description":"Token issued by the central equipme authentication, sent as `Bearer <token>`.","scheme":"bearer","type":"http"},"ApiKeyAuth":{"description":"Static key for machine-to-machine access. Alternative to BearerAuth.","in":"header","name":"X-API-Key","type":"apiKey"}},"schemas":{"middleware.ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/middleware.ErrorBody"}},"type":"object"},"middleware.ErrorBody":{"properties":{"code":{"description":"Code is the stable, machine-readable reason. Clients match on this, not on the message.","type":"string"},"details":{"description":"Details carries structured context where an error has any — a conflict names what it\ncollided with. Absent otherwise."},"message":{"type":"string"}},"type":"object"}}},"paths":{"/v1/attachments/{id}":{"delete":{"description":"Deletes an attachment and its stored file. A file that a message already refers to disappears from\nthat message. Answers with `204` and no body.\n","operationId":"deleteAttachment","parameters":[{"description":"Attachment id","in":"path","name":"id","required":true,"schema":{"type":"integer"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Delete an attachment","tags":["attachment"]}}}}
```

## Download an attachment

> Streams the file. The response is the file itself, not JSON — \`Content-Type\` is what the attachment\
> was stored as, and \`Content-Disposition\` carries its name.\
> \
> The same file is also served under \`/v1/attachments/{id}/download/{filename}\`, where the trailing\
> segment is cosmetic: it lets a browser save the file under a readable name without a further round\
> trip. Any name works, the file is chosen by its id. Attachment bodies are never cached by\
> intermediaries.<br>

```json
{"openapi":"3.1.0","info":{"title":"Solvio REST API","version":"1.0.0-beta"},"tags":[{"description":"An **attachment** is a file that belongs to a thread — uploaded first, then referenced when a message\nis posted. Uploads and messages are separate steps so that a file can be transferred while a message\nis still being written.\n\nAn attachment that is uploaded while composing carries `composing: true` and belongs to no message\nyet; posting a message with its id is what binds the two together.\n","name":"attachment"}],"servers":[{"description":"The host serving this document","url":"/api"}],"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"BearerAuth":{"description":"Token issued by the central equipme authentication, sent as `Bearer <token>`.","scheme":"bearer","type":"http"},"ApiKeyAuth":{"description":"Static key for machine-to-machine access. Alternative to BearerAuth.","in":"header","name":"X-API-Key","type":"apiKey"}},"schemas":{"middleware.ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/middleware.ErrorBody"}},"type":"object"},"middleware.ErrorBody":{"properties":{"code":{"description":"Code is the stable, machine-readable reason. Clients match on this, not on the message.","type":"string"},"details":{"description":"Details carries structured context where an error has any — a conflict names what it\ncollided with. Absent otherwise."},"message":{"type":"string"}},"type":"object"}}},"paths":{"/v1/attachments/{id}/download":{"get":{"description":"Streams the file. The response is the file itself, not JSON — `Content-Type` is what the attachment\nwas stored as, and `Content-Disposition` carries its name.\n\nThe same file is also served under `/v1/attachments/{id}/download/{filename}`, where the trailing\nsegment is cosmetic: it lets a browser save the file under a readable name without a further round\ntrip. Any name works, the file is chosen by its id. Attachment bodies are never cached by\nintermediaries.\n","operationId":"downloadAttachment","parameters":[{"description":"Attachment id","in":"path","name":"id","required":true,"schema":{"type":"integer"}},{"description":"Name to save the file under","in":"path","name":"filename","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/octet-stream":{"schema":{"type":"file"}}},"description":"OK"},"400":{"content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Bad Request"},"401":{"content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Unauthorized"},"403":{"content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Forbidden"},"404":{"content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Not Found"},"500":{"content":{"application/octet-stream":{"schema":{"$ref":"#/components/schemas/middleware.ErrorResponse"}}},"description":"Internal Server Error"}},"summary":"Download an attachment","tags":["attachment"]}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://hub.equipme.io/development/api-reference-app/attachment.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
