Storage
Every organization gets its own storage bucket, created the first time something is written to it. Files are addressed by an opaque id; you never construct a storage path yourself.
The part worth reading twice: the bucket belongs to the organization, not to Axowl’s shared pool. Where it physically lives follows the org’s storage region, and one org’s files can never land under another org’s prefix — the bucket name is looked up from a registry rather than built from a string.
Authentication
Section titled “Authentication”X-Api-Key: ah_live_…The key must be org-wide. An App-Group-scoped key gets 401 on every endpoint here, because a
file box is an organization-level resource and there is no app-group dimension to narrow it to —
honouring such a key would silently widen its contract.
Writes are gated by the organization’s plan.
| Plan | Included storage |
|---|---|
| Free | 2 GB |
| Pro | 30 GB |
| Pro+ | 100 GB |
| Business | 200 GB |
| Enterprise | Unlimited |
Two things surprise people, so they are stated plainly:
- Trashed files do not count. Deleting frees the quota immediately, even though the bytes are still recoverable.
- Restoring can fail. Because a restore adds those bytes back, it can return
storage_quota_exceededeven though the delete that preceded it always succeeded.
GET /storage/usage reports the exact number the upload gate enforces. If you want to check before
uploading rather than handle a rejection, read it first.
Endpoints
Section titled “Endpoints”GET /api/public/v1/files?trashed=&q=&page=&pageSize=POST /api/public/v1/files multipart/form-data, part "file"GET /api/public/v1/files/{id}/downloadDELETE /api/public/v1/files/{id}POST /api/public/v1/files/{id}/restoreGET /api/public/v1/storage/usageUpload
Section titled “Upload”Send multipart/form-data with a part named file. Maximum 10 MB per file.
curl -X POST https://testapi.axowl.com/api/public/v1/files \ -H "X-Api-Key: ah_live_…" \ -F "file=@quarterly-report.pdf"{ "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301", "fileName": "quarterly-report.pdf", "contentType": "application/pdf", "sizeBytes": 284119, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "uploadedAt": "2026-08-20T11:42:07.482Z", "deletedAt": null}The sha256 is ours, not yours. The server hashes the bytes it actually received; it does not
echo a digest you supplied. Compare it against your own to confirm the transfer was intact.
Filenames are sanitised — path separators and control characters are stripped, and non-ASCII names (Korean, Japanese, emoji) survive intact. The name you get back is the name that was stored.
Download
Section titled “Download”Downloads are not proxied through the API. You ask for a link and fetch the bytes directly from storage.
curl https://testapi.axowl.com/api/public/v1/files/3f2504e0-…/download \ -H "X-Api-Key: ah_live_…"{ "url": "https://…r2.cloudflarestorage.com/…?X-Amz-Signature=…", "fileName": "quarterly-report.pdf", "expiresInMinutes": 10}Three properties of that URL matter:
- It expires in 10 minutes.
- Anyone holding it can fetch the file until it expires. It is a bearer credential — do not log it, and do not put it in a redirect chain you don’t control.
- It always responds with
Content-Disposition: attachment. The file is never rendered inline, so an uploaded HTML or SVG file cannot execute as a page.
Issuing a link is recorded in the audit trail. Whether the bytes were actually fetched is not — that transfer happens between the caller and storage, so “we issued a link” is the honest limit of what we can attest.
Trash and restore
Section titled “Trash and restore”DELETE is a soft delete. The stored object is not removed.
{ "trashed": true, "retentionDays": 30, "restorableUntil": "2026-09-19T11:42:07.482Z" }| Plan | Retention |
|---|---|
| Free | 7 days |
| Paid | 30 days |
After that window the object is permanently removed. There is no version history behind it — the trash window is the only undo, which is why deletes are reversible and permanent removal is not something you can trigger through this API.
curl https://testapi.axowl.com/api/public/v1/storage/usage -H "X-Api-Key: ah_live_…"{ "usedBytes": 1288490188, "limitBytes": 2147483648, "remainingBytes": 858993460, "fileCount": 42, "trashedBytes": 10485760, "trashedCount": 3, "trashRetentionDays": 7}limitBytes and remainingBytes are null on unlimited plans.
Errors
Section titled “Errors”| Status | error | What happened |
|---|---|---|
| 401 | — | Missing/invalid ah_live_ key, or the key is App-Group-scoped |
| 400 | multipart_form_required | Body was not multipart/form-data |
| 400 | no_file | No part named file |
| 400 | file_too_large | Over 10 MB |
| 403 | storage_quota_exceeded | Plan limit would be exceeded — body carries usedBytes and limitBytes |
| 404 | — | No such file in this org (or already trashed, for endpoints that need it live) |
storage_quota_exceeded is the one worth handling explicitly: it is not a transient failure and
retrying will not help. Either free space or the organization needs a higher plan.
What this API does not do yet
Section titled “What this API does not do yet”Being explicit so you don’t go looking:
- No folders. Files sit in one flat box per organization.
- No public share links. Every read goes through a short-lived signed URL tied to your API key.
- No streaming or range requests through the API — the presigned URL supports ranges, so seek against that directly.
- No resumable or multipart upload. 10 MB is the ceiling on this path.