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
| Operation | Requires | Gets 403 |
|---|---|---|
All GET | any role | — |
POST /upload, PUT /:id | a write role | viewer |
DELETE /:id | a delete role | viewer, 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.
| Field | Required | Description |
|---|---|---|
file | Yes | The file |
code_id | No | Attach to this code (must belong to the workspace) |
visibility | No | private (default) or public |
curl -X POST https://qr3.app/v1/files/upload \ -H "Authorization: Bearer qr3_sk_..." \ -F "code_id=qr_a1b2c3d4" \ -F "visibility=public"const form = new FormData();form.append('file', fileInput.files[0]);form.append('code_id', 'qr_a1b2c3d4');form.append('visibility', 'public');
const res = await fetch('https://qr3.app/v1/files/upload', { method: 'POST', headers: { Authorization: 'Bearer qr3_sk_...' }, body: form, // do NOT set Content-Type yourself — the boundary would be missing});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.
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.
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
| Plan | Max per file |
|---|---|
| Free | 5 MB |
| Pro | 25 MB |
| Business | 100 MB |
| Agency | 100 MB |
| Enterprise | 250 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
| Status | When |
|---|---|
| 400 | Unrecognised or mismatched file signature, malformed multipart body |
| 401 | Missing or invalid API key |
| 403 | Role may not write, or may not delete |
| 404 | code_id or file not in this workspace; on download also: stored object missing |
| 413 | File exceeds the plan’s per-file limit |
| 422 | Missing file, invalid visibility, per-code file limit reached, workspace storage limit exceeded |
| 429 | Upload rate limit (100/hour/workspace) or the general rate limit |
Related
- Files & datasheets — the dashboard route
- Landing page per code