Developer documentation
Add uploads to your app in minutes
The @ibanzajoe/uploader SDK gives you a drop-in React picker and a framework-free upload client, plus delivery/transform URLs and per-file access control — all against your own account and storage.
Overview
The SDK ships two layers from one package:
- Headless core (
@ibanzajoe/uploader/core) — a framework-freeUploaderClientplus URL/transform helpers. No React dependency; use it in any framework, a Node script, or a serverless function. - React components (
@ibanzajoe/uploader) — a drop-in<PickerOverlay>modal, inline<DropPane>, ausePicker()hook, camera capture, and an in-picker image editor.
Uploads go direct to storage via a presigned URL when your plan enables it (bytes never transit our API); otherwise the client transparently falls back to a proxied upload. You don't choose — it probes and picks.
Set up the product
Before any code, grab three things from the product:
- An account. Sign up, or have an operator create one. Your plan sets your limits and which delivery-protection modes you may use.
- A public API key (
pk_…). Create it underAPI keysin your dashboard. The public key goes in client code. Each key also has a secret used only server-side to sign URLs — never ship the secret to a browser. - The API base URL (
apiUrl). The origin of your API server, e.g.https://api.yourdomain.com.
Install
npm install @ibanzajoe/uploaderreact / react-dom (v18 or v19) are optional peer deps — only needed for the React components. The /core entry has no peer deps.
Quick start
React — the picker
import { useState } from 'react'
import { PickerOverlay } from '@ibanzajoe/uploader'
import '@ibanzajoe/uploader/styles.css'
export function UploadButton() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Upload</button>
<PickerOverlay
apikey="pk_your_public_key"
apiUrl="https://api.yourdomain.com"
open={open}
onClose={() => setOpen(false)}
onUploadDone={(res) => console.log(res.filesUploaded)}
/>
</>
)
}Headless — the client
import { UploaderClient } from '@ibanzajoe/uploader/core'
const client = new UploaderClient({
apikey: 'pk_your_public_key',
apiUrl: 'https://api.yourdomain.com',
})
const result = await client.upload(file, { filename: 'photo.jpg' })
console.log(result.handle) // "abc123def456"
console.log(result.url) // delivery URL for the original⚠️ The option/prop name is apikey (all lowercase) — apiKey is not recognized.
Uploading files
client.upload(file, options?) returns a FileResult and automatically chooses direct-vs-proxied and single-shot-vs-multipart by size (multipart kicks in ~6 MiB). Use client.uploadAll(files, options?) for a concurrency-limited batch.
| Upload option | What it does |
|---|---|
onProgress | (percent: number) => void — real byte progress, 0–100 |
filename | Override the stored filename |
signal | AbortSignal to cancel (throws UploaderError code ABORTED) |
chunkSize | Multipart chunk size in bytes (default ~5 MB) |
deliveryProtection | Per-upload 'public' | 'hotlink' | 'signed' (see below) |
allowedOrigins | Per-upload origin lock (see below) |
FileResult is { handle, url, filename, mimetype, size, status }. The handle is the stable id you use to build delivery/transform URLs; url is the delivery URL for the original.
Delivery & transforms
Every file has a stable handle. FileResult.url serves the original; build a transformed derivative (resize, crop, format, quality…) with transformUrl() and the op builders. The API renders and caches the derivative on first request.
import { transformUrl, resize, output, quality } from '@ibanzajoe/uploader/core'
const thumb = transformUrl({
handle: result.handle,
apiUrl: 'https://api.yourdomain.com',
ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],
})
// -> /resize=w:200,h:200,fit:crop/output=format:webp/<handle>
// <img src={thumb} alt="" />Op builders: resize, crop, rotate, flip, flop, quality, output.
Protecting files
Each account has a delivery-protection mode that decides who can fetch a file's bytes. It's enforced on both the original and transform routes. Which modes you may use is set by your plan.
| Mode | Who can fetch | Use for |
|---|---|---|
public | Anyone with the link (default) | Public images, avatars, marketing assets |
hotlink | Only requests whose Origin/Referer is allowlisted | Store images you don't want hotlinked |
signed | Only requests carrying a valid signed read URL | Sensitive / private files |
Signed URLs
For a signed file, the read URL must carry a policy + signature pair. Your backend produces it (it holds the key secret); the SDK only assembles the URL.
import { createHmac } from 'node:crypto'
// Runs on YOUR server — it holds the API key secret. Never ship the secret to a browser.
app.post('/api/sign-read', (req, res) => {
const spec = {
expiry: Math.floor(Date.now() / 1000) + 300, // 5 min
call: ['read'],
handle: req.body.handle, // bind to one file
}
const policy = Buffer.from(JSON.stringify(spec)).toString('base64')
const signature = createHmac('sha256', process.env.UPLOADER_KEY_SECRET)
.update(policy)
.digest('hex')
res.json({ policy, signature })
})import { withSignedPolicy } from '@ibanzajoe/uploader/core'
// policy + signature come from your backend (see above)
const url = withSignedPolicy(file.url, { policy, signature })
// <img src={url} />hotlink matches Origin/Referer, which is spoofable — treat it as an anti-hotlink deterrent, not strong access control. Use signed for real access control.
Per-file protection
Need different protection for different files — most product images on hotlink, a few documents signed and locked to one domain? Choose it per file, at upload, from your own code. Set a client-level default and/or a per-upload override; per-upload wins.
// A dedicated client for sensitive uploads: signed + domain-locked.
const secure = new UploaderClient({
apikey, apiUrl,
deliveryProtection: 'signed',
allowedOrigins: ['https://app.customer.com'],
})
await secure.upload(sensitiveFile) // this file is signed + origin-locked
// The normal client stays on the account default (e.g. hotlink)…
await normal.upload(productImage) // inherits account mode
await normal.upload(oneImage, { deliveryProtection: 'public' }) // per-upload override- Omit both fields → the file inherits the account's mode (default; existing files unaffected).
- The chosen mode must be allowed by your plan, or the upload is rejected with
403 DELIVERY_MODE_NOT_ALLOWED. - For a signed file, the signature is the real gate;
allowedOriginsadds a domain lock on top (defense-in-depth). - Set once at upload — there is no "change it later" call.
React components
All components take the shared props: apikey (required), apiUrl, security?, pickerOptions?, and the callbacks onUploadDone, onFileUploadFinished, onFileUploadFailed, onCancel.
<PickerOverlay>— full-screen modal with drag-and-drop, thumbnails, progress, retry. Controlled viaopen+onClose.<DropPane>— inline drop zone you embed in a form.usePicker(options)— headless hook for a fully custom UI: returnsfiles,addFiles,upload,progress, and more.<CameraCapture>and<ImageEditor>/editImage()— capture from the device camera and crop/rotate/resize before upload (zero server cost).
Style via the --uploader-* CSS variables, or a per-instance theme prop. Dark mode auto-applies under data-theme="dark".
Error handling
Uploads throw UploaderError with a typed code and optional statusCode.
import { UploaderError } from '@ibanzajoe/uploader/core'
try {
await client.upload(file, { signal })
} catch (err) {
if (err instanceof UploaderError) {
// err.code: ABORTED | NETWORK_ERROR | SERVER_ERROR | CLIENT_ERROR | INVALID_RESPONSE
// err.statusCode: the HTTP status when available (e.g. 403, 413)
console.error(err.code, err.statusCode, err.message)
}
}CLIENT_ERROR carries the HTTP status — e.g. 403 for a disallowed mode/capability, 413 for a file over the plan limit, 429 for the upload cap.
API reference
What the SDK calls under the hood:
| Action | HTTP |
|---|---|
| Capability probe | GET /api/capabilities |
| Direct upload | POST /api/uploads/presign → PUT to bucket → POST /api/uploads/confirm |
| Proxied small file | POST /api/store |
| Proxied large file | POST /api/upload/start → /part → /complete |
| Original delivery | GET /file/:handle |
| Transform delivery | GET /<chain>/:handle |
Auth: the publishable key rides the X-Uploader-Key header; a signed policy rides X-Uploader-Policy / X-Uploader-Signature headers or query params.
Ready to build?
Create an account and an API key, then drop the picker into your app.