Skip to main content
REST API v1

EasySend API

Upload and share files programmatically. No API key required. Get a shareable link in one request.

Try it now

Quick Start

Upload a file and get a link. One command, under 60 seconds.

cURL
curl -F 'files[][email protected]' \
  https://easysend.co/api/v1/upload
Response
{
  "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

Need a key?
They are for sale at /api-keys starting at $5/month with subscription or one-time options.
Buy a Key

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.

Bearer Token Header
Header
Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx
Example
cURL
curl -F 'files[][email protected]' \
  -H "Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  https://easysend.co/api/v1/upload
What a key gives you
LimitAnonymousWith API Key
Bundle expiry3 days7 days
Max bundle size1 GB5 GB
Rate limit60 req/hr per IP300 req/hr per key
List your bundlesnot availableGET /my/bundles
Shared bot bucketnot availableany 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.

FieldWhat it meansSet by
api_key_idOwnership. 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_passwordPrivacy 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.

Owner Bearer always wins

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.

Access matrix
Bundle stateAnonymous requestOwner BearerVerified access token
Public (no password)200 with files200 with files200 with files
Password-protected, no owner keyfiles hidden (web shows password prompt, API returns files: [], /d/{id} returns 404)not applicable200 with files
Password-protected, has owner keyfiles hidden as above200 with files, ignores password gate200 with files
Setting access_password at upload
cURL
curl -F 'files[][email protected]' \
  -F 'access_password=hunter2' \
  -H "Authorization: Bearer es_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  https://easysend.co/api/v1/upload
Verifying a password without the owner key

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.

Signed thumbnail URLs

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

POST /api/v1/upload Auth: Anonymous OK

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.

Request

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.

ParameterTypeDescription
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.
Response (201)
{
  "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"
    }
  ]
}
Error Codes
400 No files / oversized 500 Storage error
POST /api/v1/upload/{upload_token} Auth: Upload token

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.

Request

Content-Type: multipart/form-data

ParameterTypeDescription
files[]required file(s) One or more files to add to the bundle.
Response (200)
{
  "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"
    }
  ]
}
Error Codes
400 No files / size exceeded 403 Invalid token / expired 500 Storage error
GET /api/v1/bundle/{short_code} Auth: Anonymous OK

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.

Response (200)
{
  "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"
    }
  ]
}
Error Codes
400 Invalid code format 404 Not found
GET /api/v1/bundle/{short_code}/status Auth: Anonymous OK

Lightweight status check. Returns expiry, size usage, and remaining space without the full file list. Useful for polling before adding more files.

Response (200)
{
  "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
}
Error Codes
400 Invalid code format 404 Not found
GET /api/v1/download/{file_id} Auth: Anonymous OK

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.

Response

Raw file stream with headers:

Content-Type: image/jpeg
Content-Disposition: attachment; filename="photo.jpg"
Content-Length: 284910
Error Codes
404 File not found or expired
DELETE /api/v1/file/{file_id} Auth: Upload token

Delete a file from a bundle. Requires the upload_token for authorization. The file is permanently removed from storage.

Authentication

Requires upload_token via Authorization: Bearer {token} header or ?upload_token={token} query parameter.

Response (200)
{
  "success": true,
  "deleted_file_id": 42
}
Error Codes
400 Invalid file_id 401 Missing token 403 Access denied / expired
GET /api/v1/my/bundles Auth: Bearer required

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.

Authentication

Requires Authorization: Bearer es_live_....

Response (200)
{
  "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.

Error Codes
401 Missing or invalid key 429 Rate limit exceeded
GET /api/v1/my/usage Auth: Bearer required

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.

Authentication

Requires Authorization: Bearer es_live_....

Response (200)
{
  "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
  }
}
Error Codes
401 Missing or invalid key 429 Rate limit exceeded
DELETE /api/v1/my/bundles/{short_code} Auth: Bearer required

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.

Authentication

Requires Authorization: Bearer es_live_....

Response (200)
{
  "success": true,
  "deleted_short_code": "aB3xZ"
}
Error Codes
401 Missing or invalid key 404 Not found or not owned 429 Rate limit exceeded

Code Examples

Complete working examples to upload a file and print the share URL.

bash
#!/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
python
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}"}
)
javascript
// 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
<?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

Upload a file right now
Pick a file, hit upload, see the live API response. This calls POST /api/v1/upload for real.
Drop files here or browse
    Response

    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:

    Header
    Authorization: Bearer e4f9a1b2c3d4e5f6...64 hex characters
    Query Parameter
    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:

    TierRate limitBundle sizeBundle 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.

    CodeMeaningWhen
    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.

    💬
    Slack Bot
    @EasySend mention
    💻
    VS Code
    Right-click to share
    GitHub Action
    CI/CD artifacts
    🎮
    Discord Bot
    /share command
    🧠
    ChatGPT GPT
    OpenAPI action
    🔍
    Raycast
    Mac file sharing
    🌐
    Browser
    Right-click links
    ⚙️
    Zapier
    5000+ apps

    All source code is on GitHub. See all integrations or read the MCP server docs.