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-free UploaderClient plus 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>, a usePicker() 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:

  1. An account. Sign up, or have an operator create one. Your plan sets your limits and which delivery-protection modes you may use.
  2. A public API key (pk_…). Create it under API keys in 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.
  3. The API base URL (apiUrl). The origin of your API server, e.g. https://api.yourdomain.com.

Install

bash
npm install @ibanzajoe/uploader

react / 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

tsx
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

ts
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 optionWhat it does
onProgress(percent: number) => void — real byte progress, 0–100
filenameOverride the stored filename
signalAbortSignal to cancel (throws UploaderError code ABORTED)
chunkSizeMultipart chunk size in bytes (default ~5 MB)
deliveryProtectionPer-upload 'public' | 'hotlink' | 'signed' (see below)
allowedOriginsPer-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.

ts
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.

ModeWho can fetchUse for
publicAnyone with the link (default)Public images, avatars, marketing assets
hotlinkOnly requests whose Origin/Referer is allowlistedStore images you don't want hotlinked
signedOnly requests carrying a valid signed read URLSensitive / 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.

ts
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 })
})
ts
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.

ts
// 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; allowedOrigins adds 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 via open + onClose.
  • <DropPane> — inline drop zone you embed in a form.
  • usePicker(options) — headless hook for a fully custom UI: returns files, 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.

ts
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:

ActionHTTP
Capability probeGET /api/capabilities
Direct uploadPOST /api/uploads/presign → PUT to bucket → POST /api/uploads/confirm
Proxied small filePOST /api/store
Proxied large filePOST /api/upload/start → /part → /complete
Original deliveryGET /file/:handle
Transform deliveryGET /<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.