With the Zaulto API you create containers programmatically and read at any time which links of your containers are online or offline – including the hoster. It is built for uploaders who want to wire Zaulto into their own tools, scripts or upload workflows.
An API token is shown exactly once at creation time and cannot be retrieved afterwards. Save it immediately in a safe place (e.g. a password manager) and never commit it to a repository or frontend code.
Always pass the token in the Authorization header – never in the URL, since URLs end up in logs and browser history. If you suspect it has leaked, revoke it in your account right away and create a new one. Every token is limited to your own account and to its type. An upload token (scope “upload”) can create containers on your behalf and read the link status of your own containers – it cannot read, change or delete anyone else's containers, and it never returns the download links themselves. A token for the reupload tool (scope “reupload”) only works on the reupload endpoints below.
The upload API authenticates with a personal bearer token. Here's how to get one: sign in, open the “API tokens” area in your account, give it a descriptive label (e.g. “My upload script”, up to 100 characters – longer ones are truncated) and optionally a lifetime in days (at most 3650 days; without one the token does not expire). The token is shown once in clear text – copy it immediately.
On every request send the header Authorization: Bearer <YOUR_TOKEN>. If the header is missing or not of the form Bearer <token>, the API responds with 401 and missing_bearer_token. If the token is unknown, revoked or expired – or your account is suspended or not yet approved – you get 401 with invalid_token. Resetting your password via “Forgot password” revokes all your tokens.
Header
Authorization: Bearer zaulto_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/jsonA POST to /api/v1/containers with a JSON body (Content-Type: application/json) creates a new link container in your account. Only title and at least one link in links are required; every other field is optional with the defaults listed below. Unknown fields are ignored. Only http and https links are accepted.
If hoster or filename is missing, Zaulto takes the host name or the file name from the URL. Links with the same file name within one link set are grouped as mirrors of the same file. After creation Zaulto starts a link check in the background – until the first check the links have the status unknown.
Rules for customSlug: 3–40 characters, only letters A–Z/a–z, digits, - and _; the first and last character must be a letter or digit. Surrounding whitespace is trimmed, an empty string is invalid – omit the field if you don't want a slug. Reserved (case-insensitive) are api, admin, assets, contact, create, crypt, dashboard, disclaimer, favicon, login, logout, new, privacy, public and static → 400 reserved_slug. Slugs are unique case-insensitively; if one is already taken – also by a deleted container, since slugs are never released – you get 409 slug_taken.
With folderId you file the container into a folder of your account (omit or null = top level). An unknown or foreign folder yields 404 folder_not_found.
The response (201) contains id (8 characters from A–Z without I and O plus the digits 2–9), slug (your customSlug or null), url (public container URL via the ID) and shortUrl (URL via the slug, null without a slug) – so you can hand out the link right away. Access protection, expiry and display are set with the fields in the next section.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/containers | Bearer token | Create a new container with one or more links. |
Request (curl)
curl -X POST https://zaulto.com/api/v1/containers \
-H "Authorization: Bearer $ZAULTO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "My release",
"links": [
{ "url": "https://filer.net/get/abc123", "filename": "part1.rar" },
{ "url": "https://ddownload.com/abc123xyz", "filename": "part2.rar" }
]
}'Response (201)
{
"id": "AB23CD45",
"slug": null,
"url": "https://zaulto.com/#/c/AB23CD45",
"shortUrl": null
}| Field | Type | Description |
|---|---|---|
| title | string (1–200) | Required. Container title. |
| links | array (1–200) | Required. Original link set (tier 0), entries as below. |
| links[].url | string (≤2048) | Required. Full http or https URL. |
| links[].hoster | string (≤200) | Optional. Default: host name from the URL. |
| links[].filename | string (≤255) | Optional. Default: file name from the URL. |
| description | string (≤2000) | Optional description (default: empty). |
| customSlug | string (3–40) | Optional custom short slug for the URL, rules see above. |
| folderId | string|null (≤64) | Folder of your account; null or omitted = top level. |
| mirrorSets | array (≤20 × ≤200) | Mirror tiers 1, 2, … as complete replacement link sets (entries like links). Visitors get the lowest tier with at least one online link. Empty sets are skipped; the numbering of the following ones is kept. |
| listed | boolean | List in the public directory (default: false). It only appears there while the container is active and not expired, has no password (not even via its folder) and uses no referrer filter. |
All fields in this section are optional and go into the same JSON body as title and links. With them you enable access protection right at creation – a password, a captcha, an expiry date or access limit – and decide what visitors see on the container page. Numbers are integers; values outside the range are rejected with 400 validation, not adjusted.
Points in time (expiresAt, autoDeleteAt) are Unix time in milliseconds; null means “never”, 0 is invalid. Once one of them is reached, the container is deleted automatically. With urlMasking the real hoster URL stays hidden; autoProtection requires masking and is stored as false without it.
Example body with protection
{
"title": "My release",
"customSlug": "my-release",
"links": [{ "url": "https://filer.net/get/abc123" }],
"mirrorSets": [[{ "url": "https://ddownload.com/abc123xyz" }]],
"password": "secret",
"captchaRequired": true,
"captchaType": "pow",
"expiresAt": 1798761600000,
"maxDownloads": 500,
"urlMasking": true
}| Field | Type | Description |
|---|---|---|
| password | string (≤200) | Password visitors need to open the container. Default: empty = no password. |
| captchaRequired | boolean | Require a captcha before opening (default: false). |
| captchaType | math|pow|question|turnstile | Arithmetic task, invisible proof-of-work, your own question or Cloudflare Turnstile (default: math). turnstile only when the operator has set it up – otherwise 400 captcha_type_unavailable. |
| captchaQuestion | string (≤200) | Only with captchaType question: the question shown to visitors. |
| captchaAnswer | string (≤200) | Only with captchaType question: the expected answer (stored hashed only). Without an answer the container shows an arithmetic challenge instead. |
| adaptiveCaptcha | boolean | Captcha only for suspicious IPs: from adaptiveThreshold accesses within 24 hours. Works without captchaRequired too; with captchaRequired the captcha applies to everyone anyway (default: false). |
| adaptiveThreshold | integer (2–1000) | Threshold for adaptiveCaptcha (default: 8). |
| honeypotEnabled | boolean | Enable an invisible decoy link for automated link collectors; hits appear in the access log, the 24 h block is off during the measurement phase (default: false). |
| expiresAt | integer|null | Expiry time (Unix time in ms). Default: null = never expires. |
| autoDeleteAt | integer|null | Time (Unix time in ms) at which the container deletes itself. Default: null. |
| maxDownloads | integer|null (1–1,000,000) | Maximum number of successful unlocks across all visitors; after that access is denied. Default: null = unlimited. |
| maxAccesses | integer|null (1–1,000,000) | The container deletes itself after this many allowed accesses. Default: null = off. |
| geoMode | off|allow|block | Country filter: allow only the countries in geoCountries or block them (default: off). With allow, visitors from an unknown country are refused. |
| geoCountries | string[] (≤250) | Two-letter country codes (ISO 3166-1), e.g. DE, AT. |
| referrerMode | off|allow | With allow, only visitors coming from a domain in referrerAllow (subdomains included); no referrer means no access (default: off). Protects against linking/embedding by other sites, not against scripts (the origin can be faked). |
| referrerAllow | string[] (≤100, each ≤253) | Allowed domains, e.g. example.com. |
| rateLimitEnabled | boolean | Enable an access limit per visitor IP (default: false). |
| rateLimitMax | integer (1–1000) | Successful accesses per IP within the window (default: 4). |
| rateLimitWindowDays | integer (1–365) | Length of the window in days (default: 7). |
| stealthMode | boolean | Refused visitors get 404 (as if the container did not exist) instead of 403 (default: true). |
| showIndividualLinks | boolean | Show individual links on the container page (default: true). |
| enableClickAndLoad | boolean | Offer Click'n'Load (default: false). |
| enableDlcFile | boolean | Offer a DLC file (default: false); automatically unavailable with urlMasking. |
| allowCopyLinks | boolean | Allow copying the links (default: true). |
| showHosterNames | boolean | Show hoster names (default: true). |
| urlMasking | boolean | URL masking: the real hoster URL stays hidden (default: false). |
| autoProtection | boolean | Interstitial page with countdown and captcha before the hoster; only with urlMasking (default: false). |
| autoProtectionSeconds | integer (3–30) | Countdown of the interstitial page in seconds (default: 5). |
| forcedLang | de|en|null | Force the container page language; null = the visitor's language (default). |
A GET to /api/v1/containers/:id/status returns, for one of your own containers, the status of every link (online, offline or unknown) together with file name and hoster – so you can detect automatically what is offline where. :id is the container ID or your slug (the slug case-insensitively). You need the same bearer token as for creating; only containers of your account can be read – anyone else's, deleted and unknown containers all get the same 404 not_found.
The response deliberately contains no download links – so even a leaked token does not reveal them. Links are sorted by tier. Per mirror tier (tiers) you see the counts and which tier is currently served to visitors: the lowest one with at least one online link, or tier 0 if no link is online anywhere. Zaulto keeps the status current via its built-in, regular link check; lastCheckedAt is the latest check (Unix time in milliseconds) and null as long as no link has been checked yet.
state becomes expired once expiresAt or autoDeleteAt is reached. You only see that briefly: expired containers are deleted automatically (on the next visitor request, otherwise within 5 minutes) – after that the request returns 404. When maxAccesses is reached the container is deleted the same way, without showing expired first. If maxDownloads is used up, on the other hand, the container stays and state remains active; visitors are refused, though.
The public endpoint GET /api/c/:id, from which the container page loads its data, by contrast gives visitors only metadata such as title and protection settings – neither links nor link status. So file names and hosters of password-protected containers stay hidden.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/v1/containers/:id/status | Bearer token | Status (online, offline, unknown) of each link of one of your containers, with file name and hoster. |
Request (curl)
curl https://zaulto.com/api/v1/containers/AB23CD45/status \
-H "Authorization: Bearer $ZAULTO_TOKEN"Response (200)
{
"id": "AB23CD45",
"slug": null,
"title": "My release",
"state": "active",
"activeTier": 0,
"lastCheckedAt": 1790000000000,
"tiers": [
{ "tier": 0, "active": true, "total": 2, "online": 1, "offline": 1, "unknown": 0 }
],
"links": [
{ "id": 101, "tier": 0, "filename": "part1.rar", "hoster": "filer.net", "status": "online", "lastCheckedAt": 1790000000000 },
{ "id": 102, "tier": 0, "filename": "part2.rar", "hoster": "ddownload.com", "status": "offline", "lastCheckedAt": 1790000000000 }
]
}| Field | Type | Meaning |
|---|---|---|
| id | string | Container ID (8 characters). |
| slug | string|null | Custom slug or null. |
| title | string | Container title. |
| state | string | active, expired (expiresAt or autoDeleteAt reached, until the automatic deletion) or disabled (locked by the Zaulto team, not publicly reachable). |
| activeTier | number | Tier visitors currently get: 0 = original, 1+ = mirror. |
| lastCheckedAt | number|null | Latest check across all links (Unix time in ms), null = never checked. |
| tiers[] | array | Per tier: tier, active, total, online, offline, unknown. |
| links[] | array | Per link: id, tier, filename, hoster, status, lastCheckedAt (null = not checked yet). |
| status: online | link status | File is reachable at the hoster. |
| status: offline | link status | File is deleted or no longer available. |
| status: unknown | link status | Not checked yet or could not be determined unambiguously. |
The reupload tool runs on your computer and knows your files locally. In the tool you select the containers whose files are on that computer (none at first). When a link in a selected container goes offline, Zaulto creates a job; the tool picks it up, uploads the file again to a hoster and reports the new URL back. Zaulto then replaces the link in the container at the same position and checks it again shortly after. You switch it on and off, pause it and follow the jobs in your account under “Reupload tool”.
These endpoints need a token of its own with the type Reupload tool (scope reupload). The tool gets it itself: login takes your account's username and password and returns a new token (same check and attempt limit as the browser sign-in; a new login from the same computer – field device – revokes that computer's old token). Alternatively you create it in your account under “API tokens”. logout revokes the calling token, “Sign the tool out everywhere” in your account revokes all reupload tokens. It only works here; the upload API rejects it with 401 invalid_token – and an upload token is rejected here the same way. These responses, too, never contain the old download URL, only file name, hoster and size.
Flow: heartbeat reports the version and the hosters the tool holds an account for. pending returns jobs of selected containers for exactly those hosters only and is empty while the tool is switched off or paused in your account. With claim the tool takes a job for 2 hours; progress reports progress and extends the claim. result closes it. Anyone else's and unknown jobs all get the same 404 not_found; if a job is already closed or no longer fits (link deleted, back online or discarded in your account), you get 410 job_closed.
The new URL in result must be http or https, point to public addresses and be on the old hoster or one of these hosters: filer.net, ddownload.com, rapidgator.net, nitroflare.com, uploady.io – otherwise 400 url_rejected with a reason (scheme, host_not_allowed or ssrf). Error texts from upload_failed are cut to one line of 200 characters with local paths removed.
Request limits per token: heartbeat 10 per minute, pending and containers 30 per minute each, selection 120 per minute, claim/progress/result together 300 in 5 minutes; beyond that 429 too_many_requests. Every response carries Cache-Control: no-store.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/reupload/login | – | { username, password, device? } ⇒ { token, username }; switches the tool on for the account (a pause stays); 401 invalid_credentials, 403 account_pending/account_suspended, 429 rate_limited. |
| POST | /api/v1/reupload/logout | Reupload token | Revokes the calling token. |
| POST | /api/v1/reupload/heartbeat | Reupload token | Sign of life with version and hosters; returns the switch state and poll interval. |
| GET | /api/v1/reupload/update | Reupload token | Current tool version for the self-update: { version, size, sha256, signature }; 404 no_release. The tool checks the signature itself. |
| GET | /api/v1/reupload/update/download | Reupload token | Program file of that version (6 per hour per token). |
| GET | /api/v1/reupload/containers | Reupload token | Your active containers for the selection: { containers: [{ containerId, title, folder, links, offline, selected }] }. |
| PUT | /api/v1/reupload/containers/:containerId | Reupload token | Select or deselect a container: { selected: true|false }. Only selected containers get jobs; deselecting ends their open jobs. |
| GET | /api/v1/reupload/pending?limit=50 | Reupload token | Open jobs of selected containers for the reported hosters (limit 1–100, default 50). |
| POST | /api/v1/reupload/jobs/:jobId/claim | Reupload token | Take a job (409 already_claimed if it is taken). |
| POST | /api/v1/reupload/jobs/:jobId/progress | Reupload token | Report progress { percent: 0–100 }, extends the claim. |
| POST | /api/v1/reupload/jobs/:jobId/result | Reupload token | Report the result; with uploaded the link is replaced. |
Heartbeat (curl)
curl -X POST https://zaulto.com/api/v1/reupload/heartbeat \
-H "Authorization: Bearer $ZAULTO_REUPLOAD_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "version": "1.0.0", "hosters": ["filer.net", "ddownload.com"] }'Response (200)
{
"enabled": true,
"paused": false,
"pollIntervalSec": 60,
"latestVersion": "1.0.0",
"username": "uploader"
}Open jobs (200)
{
"jobs": [
{
"jobId": 7,
"linkId": 102,
"containerId": "AB23CD45",
"containerTitle": "My release",
"filename": "part2.rar",
"hoster": "ddownload.com",
"hosterSize": 1048576000,
"hosterMd5": null,
"tier": 0,
"position": 2,
"offlineSince": 1790000000000
}
]
}Report a result (curl)
curl -X POST https://zaulto.com/api/v1/reupload/jobs/7/result \
-H "Authorization: Bearer $ZAULTO_REUPLOAD_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "outcome": "uploaded", "url": "https://ddownload.com/new123xyz" }'| Field | Type | Meaning |
|---|---|---|
| heartbeat: version | string (≤32) | Required. Tool version, e.g. 1.0.0. |
| heartbeat: hosters | string[] (≤20) | Hosters with a stored account, e.g. filer.net. |
| pending: jobs[] | array | jobId, linkId, containerId, containerTitle, filename, hoster, hosterSize, hosterMd5, tier, position, offlineSince. |
| filename | string | File name of the link; if missing, the file name at the hoster, otherwise empty. |
| hosterSize / hosterMd5 | number|null / string|null | Size in bytes and MD5 according to the hoster, where known – for matching the local file. |
| position | number | Position of the link within its tier (from 1). |
| offlineSince | number|null | Since when the link has been offline (Unix time in ms). |
| result: outcome | string | uploaded (with url, optional size and md5), file_missing, ambiguous, no_account or upload_failed (optional error). |
| claim/progress: claimTtlSec | number | Seconds the claim is valid (7200). |
A container's original link set holds up to 200 links, plus up to 20 mirror tiers with up to 200 links each. Titles are capped at 200, descriptions at 2000 characters, the JSON body at 1 MB (larger → 413). There is no fixed upper limit on the number of your containers.
Status reads are limited to 300 requests per token in a sliding 5-minute window; beyond that you get 429 too_many_requests. Rejected requests count too – if you keep polling at the same rate, you stay blocked. So wait until older requests drop out of the window. Requests with a missing or invalid token (401) do not count. Creating containers has no request limit of its own.
Errors come as JSON {"error": "<code>"}; with validation there is also details with formErrors (errors of the body as a whole) and fieldErrors (a list of messages per field). Exceptions: invalid JSON (400), a too large body (413) and internal errors (500) respond with {"message": "…"}. Evaluate the HTTP status code, not only the body.
| HTTP | error | Meaning |
|---|---|---|
| 201 | – | Container was created (POST). |
| 200 | – | Status returned (GET). |
| 400 | validation | Body breaks the field rules (e.g. missing title, invalid URL, value out of range); an invalid customSlug shows up as invalid_slug in details.fieldErrors.customSlug. |
| 400 | reserved_slug | customSlug is a reserved word. |
| 400 | (message) | Body is not valid JSON. |
| 401 | missing_bearer_token | Authorization header missing or not of the form Bearer <token>. |
| 401 | invalid_token | Token unknown, revoked or expired, wrong scope, or account suspended or not approved. |
| 404 | folder_not_found | folderId is not a folder of your account (POST). |
| 404 | not_found | Container unknown, deleted or not in your account (GET). |
| 409 | slug_taken | customSlug is already taken (also by a deleted container). |
| 413 | (message) | JSON body larger than 1 MB. |
| 429 | too_many_requests | More than 300 status reads within 5 minutes with one token (GET). |
| 500 | (message) | Internal error – try again later. |
Store the token server-side or in a secret store only, never in browser code or public repositories. Create a separate token with its own label per app/script – that way you can revoke a single one precisely without affecting the others.
Set a lifetime where it makes sense so a forgotten token expires automatically. Send requests over HTTPS only. And if a token ever does leak: revoke it in your account immediately – a revoked token is worthless instantly.
That's everything to wire Zaulto cleanly into your uploads: create containers via token and monitor the online/offline status automatically.
From the first concept to the finished platform – how Zaulto evolves step by step.