Quick start
Sign in at dash.tonta.io, create an uploader, and copy the generated API key. Each uploader controls its allowed domains, resizing rules, watermarking, and whether originals are retained.
Embed the uploader widget
<div class="my-uploader"></div> <script src="https://tonta.io/uploader/uploader.js" data-backend="https://tonta.io/uploader/upload.php" data-target=".my-uploader" data-api-key="YOUR_API_KEY" data-callback="handleUpload"></script> <script> window.handleUpload = { onUploadComplete(file, result) { document.querySelector('#preview').src = result.link; } }; </script>
Authentication
Every API request is authenticated with the uploader's API key. The key is tied to its uploader configuration and inherits its domain restrictions.
curl -X POST https://tonta.io/uploader/upload.php \ -H "X-API-Key: YOUR_API_KEY" \ -F "[email protected]"
- Add every origin that will embed the uploader (e.g.
studio.example.com). - Origins not on the list receive
403 Domain not allowed. - Rotate keys in the dashboard if one is ever exposed.
Upload API
| Method | Endpoint | Description |
|---|---|---|
| POST | /uploader/upload.php | Upload one or more files via multipart/form-data |
Form fields
| Field | Required | Description |
|---|---|---|
file | Yes | The file input; repeat to send multiple files. |
metadata | No | JSON blob stored alongside the file (visible in the dashboard). |
uploader_id | No | Force a specific uploader config when the key owns several. |
xmp_* | No | XMP fields — see the metadata section. |
const formData = new FormData(); formData.append('file', fileInput.files[0]); formData.append('metadata', JSON.stringify({ gallery: 'seniors', sequence: 12 })); formData.append('xmp_title', 'Sunset Portrait'); const response = await fetch('https://tonta.io/uploader/upload.php', { method: 'POST', headers: { 'X-API-Key': 'YOUR_API_KEY' }, body: formData }); const result = await response.json(); console.log(result.link);
Response
{
"success": true,
"id": "rT2F6Dn",
"link": "https://files.tonta.io/rT2F6Dn.jpg",
"sfname": "rT2F6Dn_1920.jpg",
"size": 245632,
"versions": [
{
"label": "Web",
"url": "https://files.tonta.io/rT2F6Dn_1920.jpg",
"dimensions": "1920x1280",
"format": "webp"
}
]
}All asset URLs are served from the Tonta CDN at files.tonta.io. If originals are disabled, the original block is omitted and link points to the first processed version.
Large files (direct upload)
Uploads sent through /uploader/upload.php are capped at 256 MB by the server, because the file passes through the application to be resized. For anything larger — backups, archives, video masters — use the direct upload API instead: your client sends the bytes straight to storage, so no server body limit applies.
| Method | Endpoint | Description |
|---|---|---|
| POST | /uploader/direct-upload.php | init → part (large only) → finish |
Call init with the file name and size. You get back an upload ticket in one of two modes: single (one request, up to 5 GB) or large (chunked and individually retryable, up to 10 TB).
curl -X POST https://uploader.tonta.io/direct-upload.php \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "init", "file_name": "backup.tar.gz", "file_size": 10737418240 }'
{ "mode": "large", "session": "...", "part_size": 100000000, "max_parts": 10000 }For large, request an upload URL per part with {"action":"part","session":"...","part_number":N}, send each chunk, then close the file with {"action":"finish","session":"...","part_sha1_array":[...]}. A failed part is retried on its own rather than restarting the transfer. Add "force_mode":"large" at init to chunk a smaller file too, which is worth doing on an unreliable connection.
Resize an existing image
Create an additional size from an image you already uploaded — useful when a layout needs an exact width and the standard version set doesn't have one, which is a common fix for "properly size images" audit warnings.
| Method | Endpoint | Description |
|---|---|---|
| POST | /uploader/resize.php | Generate a new version at an exact width |
curl -X POST https://uploader.tonta.io/resize.php \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "file_id": "rT2F6DnQ8xKmWp3vLs9c", "width": 637, "format": "webp", "quality": 82 }'
width is required (16–8000). format (webp, jpg, png) and quality are optional and default to the source format at quality 85. The source is the original where one was kept, otherwise the largest existing version. Requesting a size that already exists returns 409 with the existing URL rather than duplicating it.
{ "success": true, "file_id": "rT2F6DnQ8xKmWp3vLs9c_637.webp", "url": "https://<your-files-domain>/rT2F6DnQ8xKmWp3vLs9c_637.webp" }Bulk import (migrations)
Moving an existing media library across — say replacing a WordPress uploads folder — should not be done by looping the upload endpoint. Hand over the list of source URLs instead and the files are fetched, processed and stored in the background.
| Method | Endpoint | Description |
|---|---|---|
| POST | /uploader/bulk.php | create, status, cancel, retry |
curl -X POST https://uploader.tonta.io/bulk.php \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "create", "items": [ { "source_url": "https://old-site.com/wp-content/a.jpg", "source_ref": "wp-101" }, { "source_url": "https://old-site.com/wp-content/b.jpg", "source_ref": "wp-102" } ] }'
Up to 2000 items per call. Poll progress with {"action":"status","job_id":"bulk_..."}, which reports per-item state along with the new base ID and file names for each finished image — that's what you use to rewrite links on the old site. Use retry to requeue anything that failed.
source_ref. It's your own identifier for the asset (a WordPress attachment ID, for example) and it makes the import resumable: anything already imported is skipped and its existing ID returned, so a migration interrupted halfway can simply be run again without duplicating files or storage.Delete API
Use the delete endpoint to remove files created by a given uploader. Requests must include the same X-API-Key used for uploads.
| Method | Endpoint | Description |
|---|---|---|
| POST | /uploader/delete.php | Delete by smallid, smallids, or base id |
curl -X POST https://tonta.io/uploader/delete.php \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{ "smallids": ["rT2F6Dn_1920.jpg", "rT2F6Dn_1024.jpg"] }'
Alternatively, pass { "id": "rT2F6Dn" } to remove every version for that base ID. The endpoint responds with:
{ "success": true, "deleted": 2, "requested": 2, "errors": [] }Metadata & XMP
Enable “XMP Metadata Embedding” in an uploader's advanced settings to embed caption data directly into image files. Supported fields:
| Field | XMP property | Notes |
|---|---|---|
xmp_title | dc:title | Human-readable title |
xmp_description | dc:description | Caption / alt text |
xmp_keywords | dc:subject | Comma-separated keywords |
xmp_creator | dc:creator | Photographer or studio |
xmp_copyright | dc:rights | Copyright string |
If XMP embedding is disabled, these fields are ignored — but you can still store structured metadata via the metadata JSON payload for dashboard use.
Signed URLs & privacy
By default every file Tonta stores is publicly fetchable at https://files.tonta.io/<file_name> (and your white-label files.* domain). You can opt into a signed-URL access model where bare URLs return 401 and access requires a short-lived HMAC token. Policy applies in three cascading levels: album > image > variation, with variation winning, then image, then album. Default at every level is public.
The cascade in plain terms
- Album-level (whole uploader): set with
POST https://dash.tonta.io/api/policy-album.php. Affects every file. - Image-level (all variations of one image): set with
POST https://dash.tonta.io/api/policy-image.php. Overrides album. - Variation-level (a specific
{base}_{size}.{ext}file): set withPOST https://dash.tonta.io/api/policy-variation.php. Overrides image. API-only — no dashboard UI by design.
All three endpoints update Cloudflare's edge KV instantly and purge affected URLs from the global cache so changes take effect within seconds.
Common patterns
- Mark the image private at the image level.
- Override every derivative size (
_512,_1024, etc.) back to public with the variation endpoint. - Result: original 401s without a signed URL; small versions display freely in any public gallery.
- When a buyer pays, mint a signed URL for the original with a short TTL and hand it over.
- Leave album public; mark the specific image private.
- Call
https://dash.tonta.io/api/sign-url.phpwith the buyer's session TTL. - Return the signed URL to the buyer; it dies on expiry.
POST https://dash.tonta.io/api/sign-url.php
Mint a time-limited signed URL for a file.
// Headers: X-API-Key: up_xxxxxx { "base_id": "tTQg3q1nWjpxUUbDmdWI", // 20-char alphanumeric file ID "variation": "512", // "original" | "256" | "512" | "1024" | ... "ttl_seconds": 3600, // optional; min 5, max ~10 years "extension": "webp", // optional; resolved from DB if absent "domain": "files.tonta.io" // optional; defaults to files.tonta.io }
Response:
{
"success": true,
"url": "https://files.tonta.io/tTQg3q1nWjpxUUbDmdWI_512.webp?exp=1748627200&u=upl_xxx&sig=2f7c...",
"file_name": "tTQg3q1nWjpxUUbDmdWI_512.webp",
"expires_at": 1748627200,
"ttl_seconds": 3600
}If ttl_seconds is omitted, the uploader's configured default (set in the dashboard's File Access section or via https://dash.tonta.io/api/update-signed-url-ttl.php) is used.
POST https://dash.tonta.io/api/sign-urls.php
Sign many files in one request — the endpoint to use for a whole gallery of private images. It costs a single call against your rate limit no matter how many URLs come back, so there is never a reason to loop sign-url.php per image.
curl -X POST https://dash.tonta.io/api/sign-urls.php \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "base_id": "rT2F6DnQ8xKmWp3vLs9c", "variation": "original", "extension": "jpg" }, { "base_id": "rT2F6DnQ8xKmWp3vLs9c", "variation": "1024" } ], "ttl_seconds": 600 }'
Up to 500 items per call. The response holds a urls array — one entry per item with its file_name and signed url — plus an errors array for anything that could not be resolved, so one bad entry never fails the batch.
POST https://dash.tonta.io/api/policy-album.php
Set the whole uploader's default privacy.
// Headers: X-API-Key: up_xxxxxx { "policy": "private" // "public" | "private" }
Re-resolves the cascade for every file in the uploader, updates KV, and purges the affected URLs from the edge cache in the background.
POST https://dash.tonta.io/api/policy-image.php
Override one image (and all its variations).
// Headers: X-API-Key: up_xxxxxx { "base_id": "tTQg3q1nWjpxUUbDmdWI", "policy": "public" // "public" | "private" | "default" (clears override) }
POST https://dash.tonta.io/api/policy-variation.php
Override a single file (specific size/extension). API-only — too granular for the dashboard UI.
// Headers: X-API-Key: up_xxxxxx { "file_name": "tTQg3q1nWjpxUUbDmdWI_512.webp", "policy": "private" // "public" | "private" | "default" }
POST https://dash.tonta.io/api/update-signed-url-ttl.php
Set the uploader's default TTL for signed URLs (used when https://dash.tonta.io/api/sign-url.php is called without ttl_seconds, and for dashboard-rendered owner views).
// Headers: X-API-Key: up_xxxxxx { "ttl_seconds": 3600 // min 5, max ~10 years }
Gallery interaction
If an uploader has a public or password-protected gallery, the gallery mints signed URLs for its visitors server-side after they pass its own access check. Private/signed file policy does not hide files from gallery visitors — it only blocks people who don't go through the gallery (direct URL hot-linkers, scrapers). To hide files from gallery visitors too, set the gallery itself to Private.
200· correct signature, before expiry401· missing or invalid signature on a private file403· signature provided but expiry has passed
Video webhook
Tonta hands large video uploads off to serverless GPUs for processing. When enabled, completion updates the uploader record automatically. If you need your own webhook, hook into the dashboard's onUploadComplete callback and forward the result payload to your system.
Rate limits
Uploads are rate limited per API key, with a per-minute burst allowance and a per-hour sustained allowance that both scale with your plan. Normal use never approaches them — the limits exist so one runaway script cannot slow the service down for everyone.
Exceeding a limit returns 429 with a Retry-After header and a retry_after value in seconds. Wait that long and retry; nothing is lost. If you are hitting 429s routinely, you are almost certainly using the wrong endpoint for the job:
| If you are… | Use this instead |
|---|---|
| Importing an existing media library | /uploader/bulk.php — one call queues thousands of files |
| Signing a whole gallery of private images | /api/sign-urls.php — one call, up to 500 URLs |
| Sending a very large file | /uploader/direct-upload.php — goes straight to storage |
Troubleshooting
401 Invalid API key— ensure you copied the key from the uploader you're targeting.403 Domain not allowed— add the site's origin to the uploader's allowed domains.File exceeds upload_max_filesize— raise the uploader's max size or compress before upload.Storage limit exceeded— upgrade your plan or delete files via the dashboard / delete API.
Still stuck? Contact support and we'll help you debug.