Praxsuite

Files

Vincent Depassier · September 17, 2026

Every file URL Praxsuite hands out points at the gateway, never at the storage provider underneath it. The content is streamed through these routes, so nothing in your product is tied to whichever cloud is holding the bytes, and a "copy image address" never leaks a provider URL.

Auth: API key or end-user JWT — with two exceptions on download, below.

List

GET /{workspaceId}/files
{
  "files": [
    { "id": "…", "name": "invoice-2026-01", "extension": ".pdf", "createdDate": "2026-01-14T09:22:11Z" }
  ]
}

Newest first, capped at 500, and there is no pagination. For anything larger, keep your own index in a table.

Upload

POST /{workspaceId}/files/upload
Content-Type: multipart/form-data

One field, named file.

curl -X POST https://gateway.praxsuite.com/{workspaceId}/files/upload \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@invoice.pdf"
{
  "id": "…",
  "name": "invoice",
  "extension": ".pdf",
  "size": 48213,
  "createdDate": "2026-01-14T09:22:11Z"
}

Keep the id — it is what a File-type column stores, as a uuid[] value in a mutation.

Size is capped by the workspace plan, and exceeding it is a 400 naming the limit in MB.

Extension is checked against a fixed allow-list, and anything else is a 400:

.pdf .doc .docx .xls .xlsx .ppt .pptx · .txt .csv .json .xml .html .htm · .png .jpg .jpeg .gif .webp .svg .ico · .zip .rar .7z .tar .gz · .mp4 .mov .avi .mp3 .wav .ogg · .py .js .ts .cs .java .go .rs .rb

The display name is derived from the uploaded filename, stripped of any path and truncated at 100 characters.

Download

GET /{workspaceId}/files/{blobId}

Returns the bytes, streamed, with the stored content type and the original filename. Not a redirect.

Three ways to be allowed through, and any one of them is enough:

  1. The blob is marked public. Served with Cache-Control: public, max-age=300.

  2. The link is signed — ?exp=…&sig=… on the URL. This is what makes a file usable from an <img src>, an email or a webhook, none of which can send an authentication header.

  3. The caller is authenticated in the ordinary way. Served with Cache-Control: private, max-age=60.

Otherwise, 401.

Get a shareable URL

GET /{workspaceId}/files/{blobId}/url?expiresMinutes=60
{
  "id": "…",
  "name": "invoice",
  "url": "https://gateway.praxsuite.com/{workspaceId}/files/{blobId}?ttl=60&exp=…&sig=…",
  "expiresAt": "2026-01-14T10:22:11Z",
  "ttlMinutes": 60
}

expiresMinutes is clamped to 5–1440 (24 hours); the default is 60. A day is the ceiling on purpose: a signature that outlives a revoked access is not revocable in any practical sense.

The returned URL is a Praxsuite URL. You can store it in a row or paste it into a document — it is the file's stable address, and the signature on it is what expires.

Delete

DELETE /{workspaceId}/files/{blobId}
{ "deleted": true, "id": "…" }

Removes both the stored object and its record. There is no undo, and no trash.

Errors

The flat shape — { "error": "…" }.

Status

Cause

400

No file in the request, file over the plan limit, or a disallowed extension

401

Not authenticated, and the blob is neither public nor signed for

404

No such blob in this workspace, or its content is missing from storage

500

The upload could not be stored