Skip to content
Developers
Menu

API reference

Uploads

Attachments and images: ask for a slot, send the bytes to its URL, finalize, then use the upload in a message or a profile. Files are served from a separate media host or the storage bucket, never the app's origin.

Every path is relative to https://api.palloo.app/v1. Send a bot token as Authorization: Bearer palloo_bot_... unless the operation says otherwise; see Authentication.

On this page

Create attachment slots

POST /channels/{id}/attachments Bot token: Call it with a bot token (people can call it too).

Ask for upload slots for up to 10 files to attach to a message. Requires Send Messages and Attach Files; bots included.

Step 1 of the upload flow. For each file, the response gives upload_url, upload_method and upload_headers. Send the file's bytes to upload_url with exactly those headers and nothing else (no Authorization): with S3 storage the URL is a presigned URL of the bucket, which checks the size and content type it was signed for; with disk storage it is PUT /uploads/{id}/content on this server. Then call POST /uploads/{id}/finalize for each file and send the message with attachments: [{upload_id}] within 24 hours. Slots expire after an hour.

Limits: max_upload_bytes per file and daily_upload_bytes in any 24 hours (both in GET /instance), and at most 50 unfinished uploads per account.

Required permissions

Requires Send Messages and Attach Files; bots included.

Parameters

Parameters of Create attachment slots
Name In Type Description
id Required path string

Channel ID.

Request body

application/json: CreateAttachmentSlotsRequest (required)

Fields of the application/json request body
FieldTypeDescription
filesRequired array of UploadFile

at least 1 items; at most 10 items

Example request body
{
  "files": [
    {
      "filename": "string",
      "size": 1,
      "content_type": "string"
    }
  ]
}

Responses

201

One slot per file, in request order.

Body: UploadSlotList

Cache-Control header.

Example response (201)
{
  "attachments": [
    {
      "id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4A",
      "filename": "string",
      "size": 0,
      "content_type": "string",
      "upload_url": "string",
      "upload_method": "PUT",
      "upload_headers": {
        "key": "string"
      },
      "upload_expires_at": "2026-10-15T09:00:00Z",
      "expires_at": "2026-10-15T09:00:00Z"
    }
  ]
}
400

invalid_body, invalid_upload (a file without a name, a size or a valid content type) or too_many_files.

Body: Error

401

unauthenticated: the bearer token is missing, invalid or expired.

Body: Error

WWW-Authenticate header.

403

missing_permission (Send Messages or Attach Files, named in missing_permissions), timed_out (with timeout_until), dm_not_allowed in a DM, or uploads_restricted: malware scanning blocked three of this account's files in 24 hours, so its uploads are paused for a day (Retry-After says how long).

Body: Error

Retry-After header.

404

channel_not_found: no such channel, or the caller is not a member of its community or cannot view the channel.

Body: Error

409

channel_not_text, blocking_user, or upload_limit_reached (50 unfinished uploads).

Body: Error

413

file_too_large: a file is larger than media.max_upload_bytes; the message says the limit.

Body: Error

429

rate_limited (30 slot requests at once, then 1 every 2 seconds) or upload_quota_exceeded (the files declared in the last 24 hours would pass media.daily_upload_bytes); Retry-After says when to try again.

Body: Error

Retry-After header.

500

internal: an unexpected server error. Details are only in server logs.

Body: Error

503

storage_unavailable: file storage could not be reached; try again shortly.

Body: Error

Retry-After header.

Create upload slot

POST /uploads Bot token: Call it with a bot token (people can call it too).

Ask for an upload slot for an avatar, a profile banner, or a community icon or banner.

Works like attachment slots. avatar and banner are for the caller (people and bots); community_icon and community_banner need community_id and Manage Community there. The file must be a PNG, JPEG, GIF or WebP image. After finalizing, set it with PATCH /users/@me or PATCH /communities/{id}.

Request body

application/json: CreateUploadRequest (required)

Fields of the application/json request body
FieldTypeDescription
purposeRequired string

The _dark purposes are the optional dark mode variants of the avatar, the server avatar and the server icon. Each upload is used once, for the field of its purpose. They are refused with 403 dark_images_disabled while dark_images_enabled is false on GET /instance, and so is setting a _dark field to an upload or finalizing a _dark upload whose slot was created before the switch was turned off.

One of avatar, avatar_dark, banner, community_icon, community_icon_dark, community_banner, member_avatar, member_avatar_dark, member_banner, sticker

community_id ID

Required for the community purposes community_icon, community_icon_dark and community_banner (Manage Server), sticker (Manage Expressions, the stickers flag, a PNG, APNG, GIF or WebP of at most 512 KB, 413 file_too_large above; then POST /communities/{id}/stickers) and the server profile purposes member_avatar, member_avatar_dark and member_banner (membership plus Change Nickname or Manage Nicknames), refused otherwise.

filenameRequired string
sizeRequired integer

at least 1

content_typeRequired string
Example request body
{
  "purpose": "avatar",
  "community_id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4A",
  "filename": "string",
  "size": 1,
  "content_type": "string"
}

Responses

201

The slot.

Body: UploadSlot

Cache-Control header.

Example response (201)
{
  "id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4A",
  "filename": "string",
  "size": 0,
  "content_type": "string",
  "upload_url": "string",
  "upload_method": "PUT",
  "upload_headers": {
    "key": "string"
  },
  "upload_expires_at": "2026-10-15T09:00:00Z",
  "expires_at": "2026-10-15T09:00:00Z"
}
400

invalid_body, invalid_upload (unknown purpose, or community_id missing or extra) or unsupported_type (not an image).

Body: Error

401

unauthenticated: the bearer token is missing, invalid or expired.

Body: Error

WWW-Authenticate header.

403

missing_permission, or uploads_restricted: malware scanning blocked three of this account's files in 24 hours, so its uploads are paused for a day (Retry-After).

Body: Error

Retry-After header.

404

community_not_found: no such community, or the caller is not a member. The two cases are indistinguishable on purpose.

Body: Error

409

upload_limit_reached: 50 unfinished uploads.

Body: Error

413

file_too_large: a file is larger than media.max_upload_bytes; the message says the limit.

Body: Error

429

rate_limited (30 slot requests at once, then 1 every 2 seconds) or upload_quota_exceeded (the files declared in the last 24 hours would pass media.daily_upload_bytes); Retry-After says when to try again.

Body: Error

Retry-After header.

500

internal: an unexpected server error. Details are only in server logs.

Body: Error

503

storage_unavailable: file storage could not be reached; try again shortly.

Body: Error

Retry-After header.

Receive upload

PUT /uploads/{id}/content No token: Needs no token.

The upload target of a slot with disk storage. Authenticated by the slot's X-Upload-Token header.

Only reached through a slot's upload_url and upload_headers; with S3 storage files go straight to the bucket instead. Content-Type and Content-Length must be the ones the slot was created for. A slot takes one upload.

Try it: not available. This operation takes a file, and Try it sends JSON only; use curl instead.

Parameters

Parameters of Receive upload
Name In Type Description
id Required path string

Upload ID.

X-Upload-Token Required header string

Request body

*/*: string (binary) (required)

Responses

204

Received. Finalize it next.

400

upload_size_mismatch or content_type_mismatch: the body is not what the slot was created for.

Body: Error

404

upload_not_found: no such slot, or the token is wrong. These are indistinguishable.

Body: Error

409

upload_already_received.

Body: Error

410

upload_expired: the slot or the finished upload expired; start again.

Body: Error

429

rate_limited: too many attempts. Every operation a bot calls is also limited per bot (50 at once, then 50 per second), so any authenticated operation can answer this to a bot. Changes to a community's structure (creating or changing the community, channels, roles, member roles, overwrites, invites, kicks, bans, timeouts, report updates and actions, and bots) are also limited per caller (30 at once, then 1 per second) and per community (60 at once, then 1 per second), and creating communities per caller (10 at once, then 1 every 5 minutes).

Body: Error

Retry-After header.

500

internal: an unexpected server error. Details are only in server logs.

Body: Error

503

storage_unavailable: file storage could not be reached; try again shortly.

Body: Error

Retry-After header.

Finalize upload

POST /uploads/{id}/finalize Bot token: Call it with a bot token (people can call it too).

Check and process an uploaded file. Safe to call again; concurrent calls get the same result.

The server reads the file, decides its type from its content (never from the declared type), and for images decodes them, applies the EXIF orientation, strips all metadata by re-encoding, makes a thumbnail and a placeholder, and checks them against the instance's abuse hash lists. WebP images are stored as JPEG or PNG. Only the caller's own uploads can be finalized; anyone else's is 404.

Parameters

Parameters of Finalize upload
Name In Type Description
id Required path string

Upload ID.

Responses

200

The finished upload.

Body: Upload

Example response (200)
{
  "id": "01J9ZQ3V5K8M2N4P6R8T0W2Y4A",
  "purpose": "attachment",
  "state": "ready",
  "filename": "string",
  "size": 0,
  "content_type": "string",
  "channel_id": "string",
  "community_id": "string",
  "width": 0,
  "height": 0,
  "placeholder": "string",
  "animated": false,
  "expires_at": "2026-10-15T09:00:00Z"
}
400

upload_size_mismatch, content_type_mismatch (the declared type was an image, video or audio type and the content is not), unsupported_type (a profile image that is not an image), invalid_image (the content is a PNG, JPEG, GIF or WebP image that cannot be decoded; it is never stored as a plain file), image_too_large (over 40 million pixels, more than 1000 frames, more than 192 MiB once decoded such as a 16-bit PNG over 25 million pixels or a large progressive JPEG, more than 100 JPEG scans, an image file over 50 MiB, an animated avatar or icon over 1024 pixels on a side, or an animated banner over 2048 pixels on a side), upload_rejected, upload_malware (malware scanning is on and the file was blocked as malware; it is not stored, and staff see the detection in the audit log), or for stickers sticker_too_large, sticker_explicit or sticker_too_slow (decoding took longer than the 2 second limit, which can happen when the server is busy; a new upload of the same file may succeed).

Body: Error

401

unauthenticated: the bearer token is missing, invalid or expired.

Body: Error

WWW-Authenticate header.

403

terms_acceptance_required (accept the current terms with POST /users/@me/terms, then retry), uploads_restricted (malware scanning blocked three of this account's files in 24 hours, so its uploads are paused for a day), or uploads_paused (five of this account's files were refused with file_too_large_to_scan because they hit the scanner's limits within an hour, so its files other than images are not scanned or accepted until the hour is over). The upload stays unfinished; Retry-After says when to finalize it again.

Body: Error

Retry-After header.

404

upload_not_found.

Body: Error

409

upload_incomplete (the bytes have not arrived yet; finalize again after the upload) or upload_changed (the bytes were replaced while the server processed them; upload into a new slot).

Body: Error

410

upload_expired: the slot or the finished upload expired; start again.

Body: Error

413

file_too_large_to_scan: malware scanning is on and the file could not be scanned whole: it is larger than the scanner accepts and the instance refuses those (PALLOO_CLAMAV_OVERSIZE=reject), or it hit one of the scanner's limits (archive depth, file count, expanded size or scan time) and the instance refuses those (PALLOO_CLAMAV_LIMITS=reject, the default). It is not retried; send a different file.

Body: Error

429

rate_limited: too many finalize calls, or this address recently had uploads refused with upload_rejected or upload_malware (the EICAR test file does not count).

Body: Error

Retry-After header.

500

internal: an unexpected server error. Details are only in server logs.

Body: Error

503

storage_unavailable, scan_unavailable (a scanner is down; the upload is held and checked again in the background, so finalize again later and it may already be ready), or upload_processing (still being processed after 20 seconds; processing continues on the server whether or not the client waits, so finalize again after Retry-After).

Body: Error

Retry-After header.