Skip to content
Documentation

API & embedding guide

Everything you need to integrate the Tonta uploader — authenticate, send uploads, embed metadata and remove files. These endpoints reflect the current production implementation.

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

HTML
<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
curl -X POST https://tonta.io/uploader/upload.php \
  -H "X-API-Key: YOUR_API_KEY" \
  -F "[email protected]"
Domain restrictions
  • 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

MethodEndpointDescription
POST/uploader/upload.phpUpload one or more files via multipart/form-data

Form fields

FieldRequiredDescription
fileYesThe file input; repeat to send multiple files.
metadataNoJSON blob stored alongside the file (visible in the dashboard).
uploader_idNoForce a specific uploader config when the key owns several.
xmp_*NoXMP fields — see the metadata section.
JS
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

JSON
{
  "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.

MethodEndpointDescription
POST/uploader/direct-upload.phpinitpart (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
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 }'
JSON
{ "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.

Stored as-is. Files sent this way are kept byte-for-byte — no resizing, WebP conversion or version set is generated, since that requires the file to pass through the processing pipeline. Use the normal upload endpoint for images you want derivatives for. Your uploader's own max file size setting still applies, so raise it before sending very large files.

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.

MethodEndpointDescription
POST/uploader/resize.phpGenerate a new version at an exact width
cURL
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.

JSON
{ "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.

MethodEndpointDescription
POST/uploader/bulk.phpcreate, status, cancel, retry
cURL
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.

Always send 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.

MethodEndpointDescription
POST/uploader/delete.phpDelete by smallid, smallids, or base id
cURL
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:

JSON
{ "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:

FieldXMP propertyNotes
xmp_titledc:titleHuman-readable title
xmp_descriptiondc:descriptionCaption / alt text
xmp_keywordsdc:subjectComma-separated keywords
xmp_creatordc:creatorPhotographer or studio
xmp_copyrightdc:rightsCopyright 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 with POST 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

Watermark protection (the photographer case)
  • 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.
Time-limited share link
  • Leave album public; mark the specific image private.
  • Call https://dash.tonta.io/api/sign-url.php with 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.

JSON
// 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:

JSON
{
  "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
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.

JSON
// 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).

JSON
// 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.

JSON
// 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).

JSON
// 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.

Verification semantics
  • 200 · correct signature, before expiry
  • 401 · missing or invalid signature on a private file
  • 403 · 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

Common errors
  • 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.