NEXIUM Storage lets you upload, serve and manage files from any app via a simple REST API. Integrate in minutes. No AWS knowledge required.
Create an account
Sign up at console.nexiumai.io. Free, no credit card required.
Create a workspace & folder
A workspace groups your folders. A folder holds your files. Folders are called buckets in the API, workspaces are called projects.
Generate an API key
Dashboard → API Keys → New key. Copy it immediately, shown only once.
The official SDKs wrap the REST API with typed methods and zero boilerplate. The JavaScript SDK has no runtime dependencies: it uses native fetch and Web Crypto. The Python SDK falls back to urllib when requests is not installed (file uploads require requests).
Install
# npm
npm install @ainexium/storage
# pnpm
pnpm add @ainexium/storage
# yarn
yarn add @ainexium/storageUsage
import { NexiumStorage } from '@ainexium/storage'
const storage = new NexiumStorage({ apiKey: process.env.NEXIUM_API_KEY })
// Upload (with optional progress tracking)
const file = await storage.upload(bucketId, fileBlob, 'photo.jpg', {
onProgress: (pct) => console.log(pct + '%'),
})
console.log(file.id, file.url)
// List & search
const { files, total } = await storage.list(bucketId, { search: 'photo', page: 1 })
// Download URL
const url = await storage.download(file.id)
// Rename
const updated = await storage.rename(file.id, 'new-name.jpg')
// Delete
await storage.delete(file.id)
// ── Direct upload (mobile / browser) ──────────────────────────────────────
// Use this flow to upload directly to R2 without routing through your server.
// Step 1 — get a presigned upload URL (call from your backend)
const { file_id, object_key, upload_url } = await storage.presign(bucketId, {
filename: 'photo.jpg',
mime_type: 'image/jpeg',
})
// Step 2 — PUT the file directly to R2 (no auth header needed)
await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: fileBlob,
})
// Step 3 — confirm the upload so NEXIUM records the file
const saved = await storage.confirm(bucketId, {
file_id,
object_key,
filename: 'photo.jpg',
mime_type: 'image/jpeg',
size_bytes: fileBlob.size,
})
console.log(saved.url) // permanent CDN URLWebhook verification
import { NexiumStorage } from '@ainexium/storage'
// Express / Node.js example
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const payload = await NexiumStorage.verifyWebhook(
req.body, // raw Buffer
req.headers['x-nexium-signature'], // "sha256=..."
process.env.NEXIUM_WEBHOOK_SECRET,
)
const event = payload as { event: string; data: unknown }
console.log(event.event, event.data) // "file.created", { id, filename, ... }
res.sendStatus(200)
} catch {
res.sendStatus(401)
}
})Pass your API key in the Authorization header on every request. All external API endpoints are prefixed with /api/v1/ext/. Store your key in an environment variable. Never expose it in client-side code.
Authorization: Bearer nx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSend a multipart/form-data POST with a file field. Replace <bucket_id> with the ID of a folder from your dashboard.
curl -X POST \
-H "Authorization: Bearer nx_live_..." \
-F "[email protected]" \
https://api.nexiumai.io/api/v1/ext/buckets/<bucket_id>/filesResponse 201 Created:
{
"id": "87e60d98-6cde-4e8a-bc65-7ff0b448091b",
"bucket_id": "71438929-d96a-4424-a425-552ca5b7a464",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 245120,
"url": "https://cdn.nexiumai.io/71438929-d96a-4424-a425-552ca5b7a464/87e60d98-6cde-4e8a-bc65-7ff0b448091b/87e60d98-6cde-4e8a-bc65-7ff0b448091b_photo.jpg",
"created_at": "2026-08-16T00:53:23Z"
}Returns all files in a folder, ordered by most recent first. Pass an optional ?search= query parameter to filter by filename.
# All files
curl -H "Authorization: Bearer nx_live_..." \
https://api.nexiumai.io/api/v1/ext/buckets/<bucket_id>/files
# Filter by name
curl -H "Authorization: Bearer nx_live_..." \
"https://api.nexiumai.io/api/v1/ext/buckets/<bucket_id>/files?search=photo"Response 200 OK:
{
"files": [
{
"id": "87e60d98-...",
"bucket_id": "71438929-...",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 245120,
"url": "https://cdn.nexiumai.io/71438929-d96a-4424-a425-552ca5b7a464/87e60d98-6cde-4e8a-bc65-7ff0b448091b/87e60d98-6cde-4e8a-bc65-7ff0b448091b_photo.jpg",
"created_at": "2026-08-16T00:53:23Z"
}
],
"total": 1,
"page": 1,
"per_page": 24
}Returns a permanent public URL. Use it directly in an <img> tag, Image.network() in Flutter, or any HTTP client with no expiration. If public access is not configured, a presigned URL valid 1 hour is returned instead.
curl -H "Authorization: Bearer nx_live_..." \
https://api.nexiumai.io/api/v1/ext/files/<file_id>/downloadResponse 200 OK:
{ "url": "https://cdn.nexiumai.io/71438929-d96a-4424-a425-552ca5b7a464/87e60d98-6cde-4e8a-bc65-7ff0b448091b/87e60d98-6cde-4e8a-bc65-7ff0b448091b_photo.jpg" }Updates the display name of a file. The object key in R2 is unchanged, only the filename field is updated.
curl -X PATCH \
-H "Authorization: Bearer nx_live_..." \
-H "Content-Type: application/json" \
-d '{"filename":"new-name.jpg"}' \
https://api.nexiumai.io/api/v1/ext/files/<file_id>Response 200 OK: updated file object.
Whenever your users upload files from their device (mobile app, browser, desktop), use the presign flow. The file goes directly from the user's device to R2, never through your server. Use this regardless of the platform (Flutter, React Native, web, Electron…).
POST /files/presign
Get a temporary upload URL and a file_id. Valid for 15 minutes.
PUT → R2 directly
Upload the file directly to the returned upload_url. No auth header needed.
POST /files/confirm
Send the file_id, object_key and metadata to save the file record.
Step 1: POST /files/presign
curl -X POST \
-H "Authorization: Bearer nx_live_..." \
-H "Content-Type: application/json" \
-d '{"filename":"photo.jpg","mime_type":"image/jpeg"}' \
https://api.nexiumai.io/api/v1/ext/buckets/<bucket_id>/files/presignStep 2: PUT directly to R2
curl -X PUT \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg \
"<upload_url>"Step 3: POST /files/confirm
curl -X POST \
-H "Authorization: Bearer nx_live_..." \
-H "Content-Type: application/json" \
-d '{"file_id":"<file_id>","object_key":"<object_key>","filename":"photo.jpg","mime_type":"image/jpeg","size_bytes":204800}' \
https://api.nexiumai.io/api/v1/ext/buckets/<bucket_id>/files/confirmConfirm response 201 Created:
{
"id": "87e60d98-...",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size_bytes": 204800,
"url": "https://cdn.nexiumai.io/71438929-d96a-4424-a425-552ca5b7a464/87e60d98-6cde-4e8a-bc65-7ff0b448091b/87e60d98-6cde-4e8a-bc65-7ff0b448091b_photo.jpg",
"created_at": "2026-08-16T01:20:00Z"
}Permanently removes the file from storage and R2. Cannot be undone.
curl -X DELETE \
-H "Authorization: Bearer nx_live_..." \
https://api.nexiumai.io/api/v1/ext/files/<file_id>All errors return JSON with a message field.
{ "message": "unauthorized" }400Bad RequestInvalid input, check your request body or parameters401UnauthorizedMissing or invalid API key403ForbiddenYou don't have access to this resource404Not FoundThe resource doesn't exist409ConflictA folder with this name already exists in this workspace429Too Many RequestsRate limit exceeded, wait before retrying (Retry-After header provided)500Server ErrorSomething went wrong on our end