Skip to content

Files API

Overview

The Files API hosts a file on qr3 and gives it a stable URL. The QR code points at that URL — you can swap the file behind it at any time without reprinting the code.

Base URL: https://qr3.app/v1/files

Optionally attach a file to a code via code_id. That is the foundation for the landing page per code, which lists all public files of a code.

Supported types are verified by magic bytes, not by filename: PDF, PNG, JPEG, WebP, MP4, glTF/GLB, JSON. A .pdf that is not a PDF is rejected.

Roles

OperationRequiresGets 403
All GETany role
POST /upload, PUT /:ida write roleviewer
DELETE /:ida delete roleviewer, contributor

The special case is contributor: that role may create and edit, but never remove.

Upload a file

POST /v1/files/upload

Expects multipart/form-data.

FieldRequiredDescription
fileYesThe file
code_idNoAttach to this code (must belong to the workspace)
visibilityNoprivate (default) or public
Terminal window
curl -X POST https://qr3.app/v1/files/upload \
-H "Authorization: Bearer qr3_sk_..." \
-F "code_id=qr_a1b2c3d4" \
-F "visibility=public"

Response (HTTP 201):

{
"data": {
"id": "file_a1b2c3d4",
"code_id": "qr_a1b2c3d4",
"filename": "datasheet.pdf",
"mime_type": "application/pdf",
"size_bytes": 284913,
"hash_sha256": "9f86d081884c7d65…",
"visibility": "public",
"status": "active",
"created_at": "2026-03-14T12:00:00.000Z",
"download_url": "https://qr3.app/v1/files/file_a1b2c3d4/download",
"public_url": "https://qr3.app/f/file_a1b2c3d4"
},
"meta": { "request_id": "req_abc123" }
}

public_url is set only when visibility: "public" and status: "active". It needs no sign-in and works directly as the target URL of a QR code.

List files

GET /v1/files

Query: code_id and visibility, both optional. Returns the workspace’s active files, newest first.

Terminal window
curl "https://qr3.app/v1/files?code_id=qr_a1b2c3d4" \
-H "Authorization: Bearer qr3_sk_..."

Response (HTTP 200):

{
"data": [ { "id": "file_a1b2c3d4", "…": "" } ],
"meta": {
"request_id": "req_abc123",
"file_count": 3,
"total_size_bytes": 812004
}
}

Get file metadata

GET /v1/files/:id

Returns the metadata including download_url (and public_url for public files). Readable by every role.

Download a file

GET /v1/files/:id/download

Streams the contents as Content-Disposition: attachment with Cache-Control: private, no-store — the response always reflects the current file, unlike the cached public /f/:id URL.

404 happens two ways: the file does not exist in the workspace (or was deleted), or it exists but its stored object is missing.

Replace a file

PUT /v1/files/:id

Swaps the contents while keeping the same id — and with it the public /f/:id and download_url addresses. An already-printed QR code stays valid.

Terminal window
curl -X PUT https://qr3.app/v1/files/file_a1b2c3d4 \
-H "Authorization: Bearer qr3_sk_..." \

visibility, code_id and created_at are preserved; filename, mime_type, size_bytes and hash_sha256 are updated. The per-file plan limit applies as on upload; the workspace storage cap is checked against the difference. The “files per code” limit does not apply — no file is added.

Delete a file

DELETE /v1/files/:id

Soft delete: the file disappears from the listing at once, stops being downloadable, and — if public — stops being served at /f/:id.

Response (HTTP 200):

{
"data": { "id": "file_a1b2c3d4", "deleted": true },
"meta": { "request_id": "req_abc123" }
}

Limits

PlanMax per file
Free5 MB
Pro25 MB
Business100 MB
Agency100 MB
Enterprise250 MB

On top of that, each plan caps the number of files per code and the total storage per workspace. The upload rate limit is 100 uploads per hour per workspace.

Errors

StatusWhen
400Unrecognised or mismatched file signature, malformed multipart body
401Missing or invalid API key
403Role may not write, or may not delete
404code_id or file not in this workspace; on download also: stored object missing
413File exceeds the plan’s per-file limit
422Missing file, invalid visibility, per-code file limit reached, workspace storage limit exceeded
429Upload rate limit (100/hour/workspace) or the general rate limit