EasySend API
Upload and share files programmatically. No API key required. Get a shareable link in one request.
Try it nowQuick Start
Upload a file and get a link. One command, under 60 seconds.
curl -F 'files[][email protected]' \
https://easysend.co/api/v1/upload
{
"success": true,
"short_code": "aB3xZ",
"share_url": "/aB3xZ",
"upload_token": "e4f9...64 hex chars",
"expires_at": "2026-03-29T12:00:00+00:00",
"files": [{
"id": 42,
"name": "photo.jpg",
"size": 284910,
"mime_type": "image/jpeg",
"download_url": "/api/v1/download/42"
}]
}
Share the file with anyone at https://easysend.co/aB3xZ -- the link is live immediately. Files expire in 3 days by default.
Authentication
Anonymous use works for every endpoint documented below. No key, no signup, no setup. The API was built that way and stays that way.
API keys are an optional layer on top. Send one as a Bearer token and you unlock higher limits plus a small set of /my/* endpoints for listing and managing the bundles tied to that key.
Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx
curl -F 'files[][email protected]' \
-H "Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
https://easysend.co/api/v1/upload
| Limit | Anonymous | With API Key |
|---|---|---|
| Bundle expiry | 3 days | 7 days |
| Max bundle size | 1 GB | 5 GB |
| Rate limit | 60 req/hr per IP | 300 req/hr per key |
| List your bundles | not available | GET /my/bundles |
| Shared bot bucket | not available | any holder of the key sees the same bundles |
Keys are available for self-service purchase at /api-keys starting at $5/month with subscription or one-time options. Operators can also mint internal keys from the admin dashboard for staff or trusted partners.
Keys are stored hashed. The plaintext es_live_... string is shown exactly once at mint time and is not recoverable. To rotate, revoke the old key and mint a new one.
Privacy model
The API has two independent concepts. Ownership and privacy are decoupled on purpose.
| Field | What it means | Set by |
|---|---|---|
api_key_id | Ownership. The key that uploaded the bundle. Used by /my/* management endpoints. Has no effect on who can view the share link. | Automatically when you upload with a Bearer header |
access_password | Privacy gate. When set, the bundle is private everywhere: web view, /api/v1/bundle/{code}, /api/v1/download/{id}, /d/{id}, /zip/{code} and the OG image. | access_password field on upload (optional) |
By default an uploaded bundle is public and the share URL works for anyone who has it, exactly like an anonymous upload. Set access_password at upload time to make the bundle private. Privacy is enforced consistently across every endpoint that touches bundle content.
The Bearer key that uploaded a bundle bypasses the password gate everywhere. A bot that holds the key can read its own bundles without rotating through the password. This is how the dashboard pattern works: keep the key server side, send Bearer on every API call, render thumbnails using the signed thumbnail_url the API returns.
| Bundle state | Anonymous request | Owner Bearer | Verified access token |
|---|---|---|---|
| Public (no password) | 200 with files | 200 with files | 200 with files |
| Password-protected, no owner key | files hidden (web shows password prompt, API returns files: [], /d/{id} returns 404) | not applicable | 200 with files |
| Password-protected, has owner key | files hidden as above | 200 with files, ignores password gate | 200 with files |
curl -F 'files[][email protected]' \
-F 'access_password=hunter2' \
-H "Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
https://easysend.co/api/v1/upload
For users who hold the password but not the key, POST /api/verify-access exchanges {short_code, password} for an HMAC access_token tied to the bundle, the requester IP and a one hour expiry. Send the token back as X-Access-Token: <token> or as ?t=<token> on any subsequent request to that bundle's content endpoints.
When the API returns a file inside a password-protected bundle (only possible via owner Bearer), image entries include a thumbnail_url with an HMAC sig good for one hour. The URL works in an <img> tag directly with no auth header, which lets dashboards embed previews of gated content without proxying bytes through their own backend. The signature only authorizes the preview path, not the full file download.
Sequential file ids on /d/{id} and /api/v1/download/{id} are no longer walkable on password-protected bundles. Without the owner Bearer, a verified access token or a valid thumbnail signature, those endpoints return 404. Public bundles still expose file ids directly, which matches their public share URL behavior.
Endpoints
Upload one or more files and create a new bundle. Returns a share URL and an upload token you can use to add more files later.
Content-Type: multipart/form-data
Send an Authorization: Bearer es_live_... header to upload under an API key. Keyed uploads get 5 GB per bundle and a 7-day default expiry instead of 1 GB / 3 days. The request and response shape are otherwise identical.
| Parameter | Type | Description |
|---|---|---|
| files[]required | file(s) | One or more files. Repeat the field for multiple files. |
| encryptedoptional | string | Set to "1" to flag the bundle as client-side encrypted. |
| access_passwordoptional | string | Password that recipients must enter on the share page before they can view or download files. Stored hashed. No account needed to set one. |
| notify_emailoptional | string | Email address to notify when files are downloaded. Max 255 characters. Must be a valid email. |
| descriptionoptional | string | Free-form description shown on the share page. Max 1000 characters. |
{
"success": true,
"short_code": "aB3xZ",
"share_url": "/aB3xZ",
"upload_url": "/u/e4f9a1...64hex",
"upload_token": "e4f9a1...64hex",
"expires_at": "2026-03-29T12:00:00+00:00",
"files": [
{
"id": 42,
"name": "photo.jpg",
"size": 284910,
"mime_type": "image/jpeg",
"download_url": "/api/v1/download/42"
}
]
}
Add files to an existing bundle. Use the upload_token returned from the initial upload. The token is the 64-character hex string, passed as the URL path segment.
Content-Type: multipart/form-data
| Parameter | Type | Description |
|---|---|---|
| files[]required | file(s) | One or more files to add to the bundle. |
{
"success": true,
"short_code": "aB3xZ",
"share_url": "/aB3xZ",
"upload_url": "/u/e4f9a1...64hex",
"files": [
{
"id": 43,
"name": "readme.txt",
"size": 1024,
"mime_type": "text/plain",
"download_url": "/api/v1/download/43"
}
]
}
Retrieve full bundle metadata including all files, sizes, expiry, and encryption status. The short_code is the 5-character alphanumeric code from the share URL.
{
"success": true,
"short_code": "aB3xZ",
"file_count": 2,
"total_size_bytes": 285934,
"max_size_bytes": 1073741824,
"expires_at": "2026-03-29 12:00:00",
"is_expired": false,
"is_encrypted": false,
"created_at": "2026-03-26 12:00:00",
"files": [
{
"id": 42,
"name": "photo.jpg",
"size": 284910,
"mime_type": "image/jpeg",
"download_url": "/api/v1/download/42"
}
]
}
Lightweight status check. Returns expiry, size usage, and remaining space without the full file list. Useful for polling before adding more files.
{
"success": true,
"exists": true,
"is_expired": false,
"expires_at": "2026-03-29 12:00:00",
"total_size_bytes": 285934,
"max_size_bytes": 1073741824,
"space_remaining": 1073455890
}
Download a file by its numeric ID. Streams the raw file bytes with the appropriate Content-Type and Content-Disposition headers. Returns the file directly, not JSON.
Raw file stream with headers:
Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"
Content-Length: 284910
Delete a file from a bundle. Requires the upload_token for authorization. The file is permanently removed from storage.
Requires upload_token via Authorization: Bearer {token} header or ?upload_token={token} query parameter.
{
"success": true,
"deleted_file_id": 42
}
List every bundle uploaded with this API key. Newest first, capped at 100 results. Each bundle now embeds its files array (with download and thumbnail URLs) so dashboards can render previews in one round trip. Any caller holding the same key sees the same list, which is the intended pattern for multiple bots sharing a bucket.
Requires Authorization: Bearer es_live_....
{
"success": true,
"bundles": [
{
"short_code": "aB3xZ",
"share_url": "/aB3xZ",
"created_at": "2026-05-20 09:14:22",
"expires_at": "2026-05-27 09:14:22",
"total_size_bytes": 5242880,
"file_count": 2,
"view_count": 7,
"is_encrypted": false,
"password_protected": true,
"description": "nightly screenshots",
"files": [
{
"id": 2065,
"name": "chrome-2026-06-03.png",
"size": 183204,
"mime_type": "image/png",
"download_count": 0,
"download_url": "/api/v1/download/2065",
"thumbnail_url": "/d/2065?preview=1&exp=1780743002&sig=2ede3f48..."
}
]
}
]
}
thumbnail_url is only emitted for image mime types. For public bundles it is the plain /d/{id}?preview=1 path. For password-protected bundles it is signed with a one-hour HMAC so a browser can embed the preview in an <img> tag without sending a Bearer header. download_url always requires the owner Bearer header for password-protected bundles.
Return metadata about the calling key plus running totals across all bundles tied to it. Useful for dashboards and quota checks before a large upload.
Requires Authorization: Bearer es_live_....
{
"success": true,
"key": {
"label": "ci-runner-prod",
"prefix": "es_live_abcd1234",
"created_at": "2026-04-01 12:00:00",
"last_used_at": "2026-05-28 17:42:09",
"rate_limit_per_hour": 300,
"max_bundle_size_bytes": 5368709120,
"default_expiry_days": 7
},
"totals": {
"bundle_count": 42,
"total_bytes": 12884901888,
"total_files": 187
}
}
Delete a bundle that was uploaded with this API key. Returns 404 if the short code does not exist or if it is owned by a different key.
Requires Authorization: Bearer es_live_....
{
"success": true,
"deleted_short_code": "aB3xZ"
}
Code Examples
Complete working examples to upload a file and print the share URL.
#!/bin/bash
# Upload a file to EasySend
RESPONSE=$(curl -s -F 'files[][email protected]' \
https://easysend.co/api/v1/upload)
# Print the share URL
echo "$RESPONSE" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print('https://easysend.co' + d['share_url'])"
# Upload more files to the same bundle
TOKEN=$(echo "$RESPONSE" | python3 -c \
"import sys,json; print(json.load(sys.stdin)['upload_token'])")
curl -s -F 'files[][email protected]' \
https://easysend.co/api/v1/upload/$TOKEN
import requests
# Upload a file
with open("photo.jpg", "rb") as f:
resp = requests.post(
"https://easysend.co/api/v1/upload",
files={"files[]": f}
)
data = resp.json()
print(f"Share URL: https://easysend.co{data['share_url']}")
# Add another file to the same bundle
token = data["upload_token"]
with open("readme.txt", "rb") as f:
requests.post(
f"https://easysend.co/api/v1/upload/{token}",
files={"files[]": f}
)
# Delete a file
file_id = data["files"][0]["id"]
requests.delete(
f"https://easysend.co/api/v1/file/{file_id}",
headers={"Authorization": f"Bearer {token}"}
)
// Upload a file using fetch
const form = new FormData();
form.append("files[]", fileInput.files[0]);
const resp = await fetch("https://easysend.co/api/v1/upload", {
method: "POST",
body: form,
});
const data = await resp.json();
console.log(`Share: https://easysend.co${data.share_url}`);
// Delete a file
await fetch(`https://easysend.co/api/v1/file/${data.files[0].id}`, {
method: "DELETE",
headers: {
"Authorization": `Bearer ${data.upload_token}`
},
});
<?php
// Upload a file using cURL
$ch = curl_init('https://easysend.co/api/v1/upload');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => [
'files[]' => new CURLFile('photo.jpg'),
],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
echo "Share: https://easysend.co" . $data['share_url'] . "\n";
// Delete a file
$ch = curl_init("https://easysend.co/api/v1/file/" . $data['files'][0]['id']);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $data['upload_token'],
],
]);
curl_exec($ch);
curl_close($ch);
Try It
POST /api/v1/upload for real.
Upload Tokens
Separate from API keys, every freshly created bundle gets an upload_token. It is per-bundle, not per-caller, and lets you keep adding files to a bundle you just created without holding any account.
The upload_token is returned when you create a bundle via POST /api/v1/upload. It acts as a bearer token that proves ownership of the bundle. You need it for:
-
POST /api/v1/upload/{upload_token}-- adding files (token is in the URL path) -
DELETE /api/v1/file/{file_id}-- deleting files (token via header or query)
Pass the token as an HTTP header or query parameter:
Authorization: Bearer e4f9a1b2c3d4e5f6...64 hex characters
DELETE /api/v1/file/42?upload_token=e4f9a1b2c3d4e5f6...64hex
Keep your upload_token secret. Anyone with this token can add or delete files in your bundle. There is no way to regenerate it.
Rate Limits
The API includes rate limit headers in every response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Two tiers apply:
| Tier | Rate limit | Bundle size | Bundle expiry |
|---|---|---|---|
| Anonymous | 60 req/hr per IP | 1 GB | 3 days |
| With API key | 300 req/hr per key | 5 GB | 7 days |
Anonymous limits are counted per source IP. Keyed limits are counted per key, so the same key used from multiple machines shares one bucket. Send an Authorization: Bearer es_live_... header on any request to get the keyed tier. See the Authentication section for how to get a key.
If you need higher throughput for a specific use case, reach out and we will work with you.
Response Codes
All JSON responses include a "success": true|false field. Error responses also include an "error" message string.
| Code | Meaning | When |
|---|---|---|
| 200 | OK | Successful GET, DELETE, or append-upload request. |
| 201 | Created | New bundle created via POST /api/v1/upload. |
| 400 | Bad Request | Missing files, invalid format, size limit exceeded. |
| 401 | Unauthorized | Missing upload_token on endpoints that require it. |
| 403 | Forbidden | Invalid token, expired bundle, or access denied. |
| 404 | Not Found | Bundle or file does not exist, or has been deleted. |
| 500 | Server Error | Storage or database failure. Retry after a moment. |
Error response shape: {"success": false, "error": "Human-readable message"}
CLI Tool
Upload files from your terminal with a single command.
Install
curl -fsSL https://easysend.co/cli/install.sh | bash
Usage
# Upload a file
easysend photo.jpg
# Output: https://easysend.co/Ab3Kz
# Upload multiple files
easysend file1.pdf file2.png file3.zip
# Pipe from stdin
cat report.csv | easysend --name report.csv
# Copy link to clipboard
easysend photo.jpg --copy
# Get bundle info
easysend --info Ab3Kz
# Raw JSON output
easysend photo.jpg --json
Embed Widget
Add file uploads to any website with a single script tag. No API key needed.
Quick Start
<script src="https://easysend.co/widget/easysend-widget.js" data-theme="dark"></script>
<div id="easysend-upload"></div>
Options
data-theme="dark|light" - Color scheme (default: dark)
data-max-files="5" - Max files per upload (default: 10)
data-button-text="Upload" - Customize dropzone text
data-target="my-div" - Target div ID (default: easysend-upload)
Live Preview
Integrations Built with This API
These integrations all use the same API endpoints documented above. No special authentication or partner access needed.
All source code is on GitHub. See all integrations or read the MCP server docs.