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.