Skip to content

Labels and attachments

Label a thread, upload a file for a message to carry, and delete files no message carries.

Use labels to organize your queue. To include a file in a conversation, reserve it, upload it to storage, confirm it, then attach it to a message. Delete files that no message carries one at a time or per thread.

Before you start

  • A thread in your product, created with support_thread_create.
  • A platform identity with product-scoped write access for the changes and read access for verification. MCP tokens must grant the relevant operations.
  • An HTTP client that can PUT raw bytes with the required headers.

The example adds a screenshot to a shop customer’s ticket. Replace abbreviated thread and attachment ids, file details, and example label values with your own.

Labels

A label is a tag with no built-in automation or reporting behavior. Threads can carry multiple labels.

  1. Create a label with support_label_create. For example:
{ "name": "billing", "color": "#2563eb" }

name must contain 1–100 characters and be unique within the product. A duplicate returns a conflict. color is optional and accepts a six-digit hex string; unlike a status, a label can have no color.

  1. Save the returned label id. Verify it with support_label_get, or find it in support_label_list, ordered by name. A product with no labels returns an empty list; none are seeded.

Labels cannot be renamed, deleted, or archived. Choose names you can continue using.

Put labels on a thread

  1. Send up to 50 labelIds when creating the thread with support_thread_create, or updating it with support_thread_update. All labels must belong to the product. An invalid id returns not found and rejects the whole call.

labelIds replaces the entire set. Include the existing ids when adding another label, or send [] to clear the set. Repeating the same replacement leaves the same label set.

Check the result. Read the thread and confirm its labels. You can also filter support_thread_list by labelIds. A request for both billing and urgent labels matches threads carrying both. Label filters combine with all other queue filters.

Attachments

Files upload directly to storage through a signed URL. Support reserves the file and confirms the upload; it does not receive the upload bytes through its API.

Use support_attachment_create and support_attachment_finalize, or the public POST /attachments and POST /attachments/finalize routes available since Support 0.10.0. Both interfaces use the same attachment records, so you can reserve through one and finalize or attach through the other. Public API calls must come from your backend. See Public HTTP API.

1. Reserve the file

Call support_attachment_create with the thread, the file name, its exact content type and its size in bytes:

{
  "threadId": "b50f26b4…",
  "fileName": "screenshot.png",
  "contentType": "image/png",
  "sizeBytes": 184320
}

Save the returned attachment id, uploadUrl, and uploadExpiresAt. The upload URL lasts 15 minutes and grants access to that object to anyone holding it. Treat it as a credential.

The limits that apply to this call:

Limit
Value
sizeBytes
1 to 26,214,400 bytes (25 MiB), declared up front so an oversized upload is never signed for
contentType
One of eleven types, as an exact string
Pending attachments per thread
20; past that the call is refused with 429 too_many_pending_attachments

Accepted types are image/jpeg, image/png, image/gif, image/webp, image/heic, video/mp4, video/webm, video/quicktime, application/pdf, text/plain, and application/zip. Supply the exact string, without parameters such as ; charset=utf-8. Other values are rejected before signing.

The 20-pending limit applies separately to each thread. Reservations stop counting against it when their upload URLs expire.

Support derives the storage location from the product and attachment id. Only the file’s base name is used; any directory portion you send is discarded.

2. Upload the bytes

Send the file bytes to uploadUrl with PUT. This request goes directly to storage.

Two headers are mandatory, and the signature covers both:

Content-Type: image/png
x-goog-content-length-range: 0,26214400

Send the exact reserved Content-Type. Always send x-goog-content-length-range: 0,26214400, regardless of this file’s size. That value is the signed size ceiling. Storage rejects a missing or changed signed header.

If storage returns 400 MalformedSecurityHeader, check both upload headers. Calling finalize after a failed upload returns No uploaded file found because storage never accepted the bytes; reserving the attachment alone does not upload it.

3. Confirm the upload

Call support_attachment_finalize with attachmentId. Support checks whether storage has the object and reads its actual size. A missing object leaves the attachment pending. An oversized object is rejected; the stored size replaces the declared size.

Support checks the first 4 KiB against the declared type. An SVG declared as PNG, or markup declared as text/plain, is rejected. The object is deleted from storage, the attachment becomes rejected, and the call returns 400 attachment_rejected: <reason>. Reasons are signature_mismatch, not_utf8, nul_byte, and markup_in_text. This state is terminal: repeating finalize returns the same error. Correct the file, then reserve and upload it again.

Check the result. Finalize returns status: "uploaded", a confirmation timestamp, and a downloadUrl served with the verified type. Repeating finalize on an uploaded attachment returns a fresh download URL without reading storage again.

Pending attachments cannot be included in a message and do not appear in message reads.

4. Attach it to a message

Call support_message_create with up to ten attachmentIds. Each must belong to the message’s thread, be uploaded, and be unclaimed by another message.

Duplicate ids or any invalid attachment reject the whole message. For an attachment-only message, omit body; do not send an empty string. A supplied body must contain 1–50,000 characters.

Check the result. Read the message and confirm each attachment’s fileName, contentType, and sizeBytes. Every read signs a fresh downloadUrl. When a cached download URL expires, read the message again for a new one.

What Support does not do to a file

Support stores file metadata: name, declared type, confirmed size, thread, and owning message. File bytes stay in the product’s platform-provisioned storage bucket and are not written to local disk.

Support does not generate thumbnails, scan for viruses, or extract text. Apply your product’s upload-safety controls before showing files to customers.

The first-4-KiB check verifies that the bytes match the declared type. It does not establish that the content is safe; a valid PNG can pass regardless of its contents.

The limits are 25 MiB per file, ten attachments per message, and 20 pending reservations per thread. Once a message claims a file, the file stays with that message until the message is deleted; see Correct or remove a message.

Delete a file no message carries

A file that was reserved, and perhaps uploaded, but never named by a message is an orphan. From Support 0.21.0 you can delete orphans with support_attachment_delete and support_attachment_purge, which need write access, or with the matching public routes. See Public HTTP API.

Delete one file

Call support_attachment_delete with the attachment id:

{ "id": "0f2b8c4a-91d7-4e63-8a05-7c1e3d9b6f24" }

On the public API this is DELETE /attachments/{id}. The response gives the state the attachment was in and whether storage held its file:

{
  "id": "0f2b8c4a-91d7-4e63-8a05-7c1e3d9b6f24",
  "threadId": "b50f26b4…",
  "status": "uploaded",
  "objectDestroyed": true
}

status is pending, uploaded, or rejected. objectDestroyed is false when storage held no file: a reservation that was never uploaded, or a file that finalize already deleted.

Support removes the file from storage first and the record second, so a download URL issued earlier stops working at once.

Status
Means
404
No such attachment, or it belongs to another product. Deleting the same attachment a second time also returns 404
409
attachment_claimed: — a message carries the file. Delete the message instead, with support_message_delete (your own) or support_message_delete_any (admin); that removes its files with it
503
attachment_cleanup_failed: — storage could not remove the file. The record is kept and nothing is lost; repeat the call

The call honours the Idempotency-Key header: a repeat with the same key returns the first 200 again. A 503 is not stored, so a retry runs again.

Delete every orphan of a thread

Call support_attachment_purge with a thread id. It deletes every file of that thread that no message carries, in any state:

{ "threadId": "b50f26b4…" }

On the public API this is POST /attachments/purge. The response counts what happened:

{
  "threadId": "b50f26b4…",
  "deleted": { "pending": 1, "uploaded": 2, "rejected": 0 },
  "objectsDestroyed": 3,
  "objectsMissing": 0,
  "skippedClaimed": 4
}
  • deleted counts the records removed, by the state they were in.
  • objectsDestroyed counts the files removed from storage; objectsMissing counts deleted records whose file storage no longer held or never received.
  • skippedClaimed counts files that messages carry. Purge leaves them alone; delete their messages, then purge again.

A thread that does not exist or belongs to another product returns 404. A second purge of the same thread returns zeros. If storage fails part-way, the call returns 503 attachment_cleanup_failed:; files already removed stay removed, and repeating the call finishes the rest. Idempotency-Key is honoured here too.

Purge cannot tell a forgotten file from one a customer is attaching right now, and it has no “older than” option. On a thread still in use it can remove that file. The message that names it is then refused with 400 “Attachments not available for this message”, and the file must be uploaded again.

Cleanup Support does on its own

An hourly background job deletes orphans in any state once 24 hours have passed since the later of the reservation and the upload. An orphan is therefore removed between 24 and about 25 hours after its last activity, so an unused reservation does not stay in storage.

A file deleted this way, or by either call above, is removed from the storage bucket at once, and download URLs stop working immediately. The bucket keeps deleted bytes for 7 more days (soft delete), so the bytes are gone for good at most 7 days after deletion.

An end-user erasure request also removes the orphans in that end user’s threads, together with the files their messages carry.

Erase an end user’s files

To remove every file an end user uploaded through Support without erasing the end user, call from your backend:

  1. support_message_delete_any for each of the end user’s messages. This removes each message’s files with it.
  2. support_attachment_purge for each of the end user’s threads.

Check the result. skippedClaimed is 0 in every purge response. A non-zero count means a message still carries a file, typically a message from someone else on the same thread. Delete that message too, or decide it is not the end user’s data, and purge again.

A Lepsto erasure request for the end user does steps like these for you.

Where to go next

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select