Attachments

File bytes never pass through the poolse API. You ask poolse for a presigned URL, upload the file straight to storage with it, then send a message that lists the attachment's id. Every endpoint here takes an end-user JWT.

The full flow, with code, is in the Attachment uploads guide.

  1. POST /v1/attachments/upload-url with the file's type and size. You get an attachment in status pending and an upload URL.
  2. PUT the bytes to upload.url with upload.headers.
  3. Send a message with attachment_ids: [id]. The attachment is linked to the message and its status becomes ready.

poolse does not check that step 2 happened before step 3. If the upload failed, the message links an attachment with no file behind it.

What is enforced, and when. The upload URL does not lock the file's type or size: storage accepts whatever is PUT. Your plan's limits are checked against the byte_size you declare in step 1. In step 3, poolse reads the stored file's size: if it is larger than the declared byte_size, the send is refused with 422 send_failed and the message contains attachment_size_mismatch. If storage cannot be reached for that check, the send goes ahead.

An attachment still pending (never linked to a message) 24 hours after it was requested is deleted automatically, file included.

Where the file goes. Into poolse's hosted storage, or, if the workspace has connected its own S3-compatible bucket, into that bucket. The endpoints and responses are the same either way; only the host in upload.url and the download URL differs. The plan quotas attachment_too_large and attachment_total_cap_exceeded apply to hosted storage only. See Bring your own storage.

The attachment object

{
  "id": "a3c9e1f0-5b7d-4e2a-8c6f-0d1b3e5a7c92",
  "tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
  "message_id": null,
  "sender_id": "11111111-2222-3333-4444-555555555555",
  "content_type": "image/png",
  "byte_size": 142812,
  "sha256": null,
  "original_filename": "screenshot.png",
  "status": "pending",
  "inserted_at": "2026-06-02T08:13:55.123456Z",
  "updated_at": "2026-06-02T08:13:55.123456Z"
}
FieldTypeDescription
iduuidPass it in a message's attachment_ids.
message_iduuid | nullThe message it is linked to. null until then.
sender_iduuid | nullThe uploader. Only they can link it to a message.
content_typestringAs requested.
byte_sizeintegerAs requested.
sha256string | null
original_filenamestring | nullAs requested.
status"pending" | "ready"ready once linked to a message.
inserted_at, updated_atstringISO 8601.

Request an upload URL

POST/v1/attachments/upload-url
End-user JWT

Request body

FieldTypeRequiredDescription
content_typestringyesMIME type, 1–255 characters. Returned in upload.headers and stored on the attachment. It is not checked against the uploaded file.
byte_sizeintegeryesFile size in bytes. Greater than 0 and at most 100 MiB, and within your plan's limits. The file is checked against it when it is attached to a message.
original_filenamestringnoUp to 512 characters.

Response

201:

{
  "attachment": {
    "id": "a3c9e1f0-5b7d-4e2a-8c6f-0d1b3e5a7c92",
    "tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
    "message_id": null,
    "sender_id": "11111111-2222-3333-4444-555555555555",
    "content_type": "image/png",
    "byte_size": 142812,
    "sha256": null,
    "original_filename": "screenshot.png",
    "status": "pending",
    "inserted_at": "2026-06-02T08:13:55.123456Z",
    "updated_at": "2026-06-02T08:13:55.123456Z"
  },
  "upload": {
    "url": "https://…",
    "method": "put",
    "headers": { "content-type": "image/png" }
  }
}

The upload URL is valid for 15 minutes. Send the bytes with an HTTP PUT (the method value is lowercase) and every header in upload.headers.

curl -X POST https://api.poolse.dev/v1/attachments/upload-url \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"content_type":"image/png","byte_size":142812,"original_filename":"screenshot.png"}'

# then, with the returned URL:
curl -X PUT "$UPLOAD_URL" -H "content-type: image/png" --data-binary @screenshot.png

Errors

CodeStatusWhen
validation_failed422byte_size missing or not a positive integer, or content_type missing.
byte_size_too_large422byte_size over 100 MiB.
attachment_too_large402Over your plan's per-file limit for hosted storage. details: { bytes, cap }.
attachment_total_cap_exceeded402Would exceed your plan's hosted storage. details: { used, would_use, cap }.
subscription_inactive402Your subscription is past due or cancelled.
attachments_not_in_plan403Your plan does not include attachments.
upload_request_failed422Storage could not issue the URL.

Get a download URL

GET/v1/attachments/:id/download-url
End-user JWT

Returns a presigned URL to download the file. It is valid for 1 hour.

Who may call it: any member of the conversation the attachment's message is in. Before it is linked to a message, only the uploader.

200:

{
  "url": "https://…",
  "method": "get"
}
CodeStatusWhen
forbidden403The caller may not read it.
not_found404No such attachment in the tenant.
storage_error502Storage could not issue the URL. Retry.

Delete an attachment

DELETE/v1/attachments/:id
End-user JWT

Deletes the attachment and its file. The file deletion is best-effort: if storage fails, the attachment is still gone from poolse.

Who may call it: the uploader, and once it is linked to a message, also an owner or admin of that conversation.

204 on success.

CodeStatusWhen
forbidden403The caller may not delete it.
not_found404No such attachment in the tenant.

Deleting a message already deletes its attachments. See Delete a message.

Attachments · API · poolse docs