Skip to content

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.

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.

PlanIncluded storage
Free2 GB
Pro30 GB
Pro+100 GB
Business200 GB
EnterpriseUnlimited

Two things surprise people, so they are stated plainly:

  1. Trashed files do not count. Deleting frees the quota immediately, even though the bytes are still recoverable.
  2. Restoring can fail. Because a restore adds those bytes back, it can return storage_quota_exceeded even 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.

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}/download
DELETE /api/public/v1/files/{id}
POST /api/public/v1/files/{id}/restore
GET /api/public/v1/storage/usage

Send multipart/form-data with a part named file. Maximum 10 MB per file.

Terminal window
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.

Downloads are not proxied through the API. You ask for a link and fetch the bytes directly from storage.

Terminal window
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.

DELETE is a soft delete. The stored object is not removed.

{ "trashed": true, "retentionDays": 30, "restorableUntil": "2026-09-19T11:42:07.482Z" }
PlanRetention
Free7 days
Paid30 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.

Terminal window
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.

StatuserrorWhat happened
401Missing/invalid ah_live_ key, or the key is App-Group-scoped
400multipart_form_requiredBody was not multipart/form-data
400no_fileNo part named file
400file_too_largeOver 10 MB
403storage_quota_exceededPlan limit would be exceeded — body carries usedBytes and limitBytes
404No 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.

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.