Attachments API
Create, resume, download, and delete attachments across Qarion resources.
Product And Ticket Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /catalog/spaces/{slug}/products/{product_id}/attachments | List product attachments. |
POST | /catalog/spaces/{slug}/products/{product_id}/attachments | Upload a product attachment with legacy multipart form data. |
POST | /catalog/spaces/{slug}/products/{product_id}/attachments/transfers | Initiate a resumable product attachment upload. |
POST | /catalog/spaces/{slug}/products/{product_id}/attachments/transfers/{transfer_id}/complete | Finalize a stored transfer as a product attachment. |
GET, HEAD | /catalog/spaces/{slug}/products/{product_id}/attachments/{attachment_id}/download | Download product attachment content or metadata. |
DELETE | /catalog/spaces/{slug}/products/{product_id}/attachments/{attachment_id} | Delete a product attachment. |
POST | /issues/tickets/{ticket_id}/attachments/transfers | Initiate a resumable ticket attachment upload. |
POST | /issues/tickets/{ticket_id}/attachments/transfers/{transfer_id}/complete | Finalize a ticket attachment. |
GET, HEAD | /issues/attachments/{attachment_id}/download | Download ticket attachment content or metadata. |
DELETE | /issues/attachments/{attachment_id} | Delete a ticket attachment. |
Equivalent initiate and complete routes are available on supported data domains, master-data datasets, meetings, document pages, analyses and analysis assets, repositories, contracts, and data-question messages. The domain route authorizes the parent before it creates or finalizes a transfer.
Product Attachment Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"product_id": "9afb13a6-7dc1-4db9-a55f-9dbf16b73ed0",
"repository_id": null,
"repository_kind": null,
"user_id": "96dd87f5-eb95-47cf-a23e-dfa8b4d3035d",
"file_name": "data_dictionary.pdf",
"file_type": "application/pdf",
"file_size_bytes": 245760,
"storage_uri": "s3://managed-location",
"storage_backend": "s3",
"created_at": "2026-08-03T11:00:00Z",
"markdown_snippet": null,
"entity_type": "product"
}
file_data and provider credentials are never included in attachment
responses. Treat storage_uri as opaque and use the authorized download route.
Resumable Upload
Use the transfer protocol for new integrations. It keeps progress durable, allows safe part retries, and avoids buffering the complete file in one API request.
1. Initiate
POST /catalog/spaces/{slug}/products/{product_id}/attachments/transfers
Content-Type: application/json
{
"file_name": "data_dictionary.pdf",
"content_type": "application/pdf",
"total_size_bytes": 245760,
"sha256": "a6e8f5632ce243d4d1808683951b6d286c4e0784fa77a47b624d0fe590a50f5f"
}
The response selects sequential or multipart upload mode and reports the
server-owned chunk size, parallelism limit, committed parts, revision, status,
and expiry time:
{
"id": "3023f516-45bb-46f6-82db-0769cd615ca4",
"storage_family": "default",
"purpose": "attachment",
"target_kind": "product",
"target_id": "9afb13a6-7dc1-4db9-a55f-9dbf16b73ed0",
"storage_backend": "s3",
"upload_mode": "multipart",
"file_name": "data_dictionary.pdf",
"content_type": "application/pdf",
"total_size_bytes": 245760,
"chunk_size_bytes": 8388608,
"maximum_parallel_parts": 4,
"expected_sha256": "a6e8f5632ce243d4d1808683951b6d286c4e0784fa77a47b624d0fe590a50f5f",
"actual_sha256": null,
"committed_bytes": 0,
"committed_parts": [],
"revision": 0,
"status": "initialized",
"expires_at": "2026-08-04T11:00:00Z",
"created_at": "2026-08-03T11:00:00Z",
"completed_at": null
}
2. Upload Parts
Read the latest transfer state with:
GET /storage/transfers/{transfer_id}
Split the file using the returned chunk_size_bytes. Part numbers start at 1.
Send the exact byte interval for each part:
PUT /storage/transfers/{transfer_id}/parts/1
Content-Type: application/octet-stream
Content-Range: bytes 0-245759/245760
Idempotency-Key: 3023f516-45bb-46f6-82db-0769cd615ca4:1
X-Chunk-SHA256: a6e8f5632ce243d4d1808683951b6d286c4e0784fa77a47b624d0fe590a50f5f
<raw bytes>
Use a stable, part-specific Idempotency-Key when retrying. After a timeout,
read the transfer again before sending the part: it may already be present in
committed_parts. Do not exceed maximum_parallel_parts, and upload parts in
order when upload_mode is sequential.
3. Finalize The Attachment
After committed_bytes equals total_size_bytes, finalize through the same
authorized domain route used to initiate the transfer:
POST /catalog/spaces/{slug}/products/{product_id}/attachments/transfers/{transfer_id}/complete
Content-Type: application/json
{
"metadata": {}
}
Finalization verifies the transfer target and owner, completes provider state, and creates the attachment record. It is idempotent: retrying a successful completion returns the same registered attachment.
Cancel an incomplete transfer with:
DELETE /storage/transfers/{transfer_id}
Transfer reads, part writes, and cancellation are owner-scoped. Provider checkpoint data and storage credentials are never returned by the API.
Storage Capabilities
Authenticated clients can inspect safe upload capabilities before initiating a domain transfer:
GET /storage/capabilities?family=default
{
"storage_family": "default",
"storage_backend": "s3",
"resumable": true,
"upload_modes": ["multipart", "sequential"],
"chunk_size_bytes": 8388608,
"maximum_parallel_parts": 4
}
Treat this response as advisory. The initiated transfer is authoritative for mode, chunk size, expiry, and parallelism. Base64 storage does not support resumable uploads; use the legacy one-request endpoint for that backend.
Legacy Multipart Upload
POST /catalog/spaces/{slug}/products/{product_id}/attachments
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | File to upload (maximum 10 MB). |
curl -X POST "https://api.qarion.com/catalog/spaces/analytics/products/{id}/attachments" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@data_dictionary.pdf"
Downloading
GET /catalog/spaces/{slug}/products/{product_id}/attachments/{attachment_id}/download
The route streams file content or redirects to a provider URL, depending on
storage policy. It supports HEAD and a single standard byte range such as
Range: bytes=1048576-. A satisfiable proxied range returns 206 with
Content-Range and Accept-Ranges: bytes; an invalid range returns 416.
Supported File Types
The approved content types include:
- Documents: PDF, Word, Excel, PowerPoint
- Images: PNG, JPEG, WebP
- Data and source: CSV, JSON, XML, YAML, SQL, Python, R, JavaScript, TypeScript, shell, PowerShell, Markdown, and plain text
- Other: Jupyter notebooks, diagrams, and generic binary content
Maximum attachment size: 10 MB.
Error Responses
| Status | Description |
|---|---|
400 | Malformed legacy upload. |
403 | Not authorized or not the transfer owner. |
404 | Attachment, transfer, or parent entity not found. |
409 | Transfer state, byte range, target, or idempotency conflict. |
410 | Transfer expired. |
413 | File or upload part is too large. |
416 | Download byte range is not satisfiable. |
422 | Unsupported content type, invalid metadata, or malformed Content-Range. |