Your files.
One API away.
Upload a file. Get a direct link. Build it into whatever you’re making.
Your first yoink
QuickstartTwo requests, one shareable URL. This example uploads hello.txt as a guest, then prints its public link.
Files are public to anyone with the link. There’s no list or delete API, and no guaranteed retention period.
set -eu
printf 'Hello, Yoink!\n' > hello.txt
response=$(curl --fail-with-body --silent --show-error --retry 2 \
https://yoink.zip/api/v1/presign \
-H 'Content-Type: application/json' \
-d '{"filename":"hello.txt","contentType":"text/plain"}')
curl --fail-with-body --silent --show-error --retry 2 \
--request PUT "$(printf '%s' "$response" | jq -er '.uploadUrl')" \
-H "Content-Type: $(printf '%s' "$response" | jq -er '.contentType')" \
--data-binary @hello.txt
printf '%s' "$response" | jq -er '.publicUrl'Using this from another website? Send uploads from your backend. Direct browser PUT uploads aren’t enabled for other origins.
Account optional.
Guest uploads work without credentials. To attribute uploads to a signed-in Yoink account, send its current Firebase ID token with the presign request.
Authorization: Bearer <FIREBASE_ID_TOKEN>Already signed in?
Use a fresh ID token from the Firebase SDK’s getIdToken() for the yoink-zip project. Tokens from another Firebase project won’t work. There are no API keys to create.
Same link, same visibility.
Authentication attributes the upload; it doesn’t make the file private. Invalid or expired credentials return 401, not a guest upload. Send the token to Yoink only, never to the storage PUT URL.
Create an upload URL
https://yoink.zip/api/v1/presignRequest a short-lived URL for one file. Send JSON with Content-Type: application/json. This step doesn’t upload any bytes.
Request body
filenamestringrequired- The file’s name. Yoink strips directory components, cleans unsupported filename characters, and limits the name to 200 characters.
contentTypestring- The MIME type to store, such as
image/png. Defaults toapplication/octet-stream. sizenumber- File size in bytes, when known. Accepted range:
0–5,368,709,120(5 GiB). This is a declaration, not a verification of the uploaded bytes.
{
"filename": "hello.txt",
"contentType": "text/plain",
"size": 14
}Response 200 OK
{
"uploadUrl": "https://<account>.r2.cloudflarestorage.com/<signed-path>",
"path": "anon/<timestamp>/<id>/hello.txt",
"publicUrl": "https://files.yoink.zip/anon/<timestamp>/<id>/hello.txt",
"contentType": "text/plain",
"expiresIn": 900
}uploadUrlstring- Temporary, signed PUT URL. Use the full returned value unchanged, including its query string. Keep it private until you’ve finished uploading.
publicUrlstring- Ready-to-use, encoded public link. Share it only after the PUT succeeds.
pathstring- Generated storage key, with an account or guest prefix, timestamp, random ID and cleaned filename. Save the returned value rather than constructing it.
contentTypestring- The exact
Content-Typeheader to send with your PUT. expiresInnumber- Always
900seconds (15 minutes). This is the upload URL’s lifetime, not the file’s lifetime.
Send the bytes
{uploadUrl}Send the raw file as the request body, using the returned contentType. The signed URL already authorizes this request: leave out the Firebase bearer token.
curl --fail-with-body --request PUT "$UPLOAD_URL" \
-H "Content-Type: $CONTENT_TYPE" \
--data-binary @hello.txtSet UPLOAD_URL and CONTENT_TYPE to the values returned by presign, or use the complete quickstart.
Raw bytes. Nothing extra.
Don’t wrap the file in JSON or FormData. The content type must match exactly. Check for a successful HTTP status; the PUT doesn’t return Yoink JSON.
One file, one PUT.
The single-PUT limit is 5 GiB. Request the signed URL just before uploading. If it expires, get a new one and restart. Multipart and resumable uploads aren’t exposed by this API.
A link that gets to the point.
{publicUrl}After a successful upload, use publicUrl to fetch the file directly from files.yoink.zip. No download token or viewer page in the way.
Made for hotlinking
Use the returned URL in an image source, a download link, or an HTTP client. Set the correct MIME type when you presign so clients can handle the file correctly.
Public links don’t inherit the signed upload URL’s 15-minute expiry. There’s no file-expiration setting or permanence guarantee. Keep your own copy.
Direct upload
Legacyhttps://yoink.zip/api/v1/uploadA single-request alternative for small files. It buffers the full body in the Worker and is subject to lower platform limits. Use presign for larger files.
Request, response & headers
Send raw bytes, not multipart form data. Set x-filename to the file name; it defaults to file. The same optional bearer authentication applies.
curl --fail-with-body --silent --show-error \
https://yoink.zip/api/v1/upload \
-H 'Content-Type: text/plain' \
-H 'x-filename: hello.txt' \
--data-binary @hello.txt{
"success": true,
"path": "anon/<timestamp>/<id>/hello.txt",
"url": "https://files.yoink.zip/anon/<timestamp>/<id>/hello.txt"
}This route returns url, not publicUrl. Your Content-Type is preserved as file metadata; absent or empty values default to application/octet-stream. Set it explicitly with cURL, which otherwise sends application/x-www-form-urlencoded.
When a request goes sideways
Handled upload API errors use a JSON error field. Storage PUTs, platform errors and unmatched routes may return XML, HTML or plain text instead. Check the status before parsing.
{ "error": "Unauthorized" }Invalid request
Presign expects a JSON object with a non-empty filename. Check your JSON syntax and Content-Type header.
Unauthorized
A supplied credential is invalid, expired or malformed. Refresh the Firebase ID token. For an intended guest upload, omit credentials entirely.
File too large
Presign rejects a declared size below zero or above 5 GiB. Platform request limits may also reject direct uploads.
Server error
Wait briefly and retry. Presign can return this when upload storage isn’t configured. Keep the response body if the problem persists.
Storage returned 403?
Check that the signed URL hasn’t expired, the Content-Type is unchanged, and no bearer header was attached. Use the complete URL, including its signature.
Retry without surprises.
Retry transient network failures and 5xx responses with a short, increasing delay and a limit on attempts. Retry a PUT at the same URL while it’s valid; a new presign creates a new path. Don’t retry a validation error unchanged.