Upload and use attachments
Prepare a file, upload it directly, finalize the exact version, and wait for a clean scan.
Before you begin
You need an existing ticket and current workspace ticket access, using a
bearer access token or an authorized agent/full_access
API key. A staff_admin key cannot access ticket
attachments. Send X-Helpdesk-Tenant-Id with every CaseKit
request.
Call the JSON API from your server. File bytes travel directly to object storage using the signed URL returned by the API. Browser uploads also require an allowed bucket origin; the signed URL does not bypass CORS.
1. Prepare the upload
Calculate the SHA-256 digest of the exact file bytes and encode the
digest as canonical base64 (44 characters), not hexadecimal. Call
POST /v1/tickets/{ticketId}/attachments/prepare with JSON
and an Idempotency-Key:
{
"fileName": "example.txt",
"contentType": "text/plain",
"byteSize": 123,
"checksumSHA256": "{base64DigestOfTheExactFileBytes}"
}
Replace the example size and checksum with actual values. Files must be
between 1 byte and 10 MiB. A ticket can have at most 20 live prepared
uploads. The idempotency key is 1–128 trimmed characters matching
^[A-Za-z0-9][A-Za-z0-9._:-]*$; a UUID is suitable. Reuse
the same key and payload after an uncertain response.
The 201 response contains attachmentId,
uploadUrl, method (PUT),
headers, and expiresAt. Use those returned
values; the upload URL lasts at most 15 minutes.
2. Upload the exact bytes
Send a PUT to uploadUrl with the raw file body
and returned headers: content-type,
content-length, x-amz-checksum-sha256, and
if-none-match: *. Do not send multipart form data or
forward CaseKit credentials to the upload URL.
In a browser, validate the file size against the returned content
length, let the browser set Content-Length, and set the
other required headers exactly. Omit cookies/credentials and reject
redirects. Save the successful response’s x-amz-version-id;
you need it for finalization. Do not assume an upload succeeded if that
version cannot be obtained.
3. Finalize that version
Call
POST /v1/tickets/{ticketId}/attachments/{attachmentId}/finalize:
{ "objectVersionId": "{x-amz-version-id-from-upload}" }
No If-Match or idempotency header is required here. The API
verifies the version, size, content type, and checksum, then returns
200 {"attachment": …}. Finalization registers a pending
scan; it does not mean the file is clean.
4. Wait for scanning, then use the attachment
List attachments using
GET /v1/tickets/{ticketId}/attachments. Follow
nextCursor until null, including after empty pages. Scan
states are pending, clean,
infected, and failed. Refresh with bounded
backoff while pending; do not treat a failed or infected scan as usable.
Include clean IDs in attachmentIds when posting a reply or
an internal note. These ticket mutations require a current quoted
version in If-Match and an Idempotency-Key,
plus the remaining fields shown in the reference. Each accepts at most
five distinct clean attachments totaling at most 10 MiB. Ticket creation
does not accept attachment IDs; create the ticket first.
A successful reply records the reply and any planned delivery. It does not prove the email was delivered. Internal notes are not emailed.
5. Download a clean attachment
Call
GET /v1/tickets/{ticketId}/attachments/{attachmentId}/download. The response contains url,
expiresInSeconds, fileName, and
contentType. Fetch from the returned URL; attachment bytes
are not proxied through the API. The default download lifetime is 60
seconds; use the returned expiry.
The API requires clean scan metadata and the clean tag for the exact
object version. Pending, unsafe, missing, or denied downloads return a
generic 404. Signed URLs grant access until expiry;
revoking an API key prevents new URLs but does not recall an existing
one.
Discarding an unused upload
DELETE /v1/tickets/{ticketId}/attachments/{attachmentId}
discards only a prepared, unbound upload and returns
{"discarded":true}. It requires no version or idempotency
header. A finalized attachment belongs to the ticket record and cannot
be deleted through this endpoint.
For request schemas and errors, use Attachments and Ticket conversation in the API reference.