Upload a hosted file

Upload a file once and link it from your emails instead of attaching it.

Upload a file to wemail's hosted storage and put the returned url on a link or button in your email HTML. Unlike an attachment — which every recipient receives a full copy of, so its size is multiplied by your recipient count — a hosted file is stored once and downloaded on demand. A 2.5 MB report sent to 5,000 recipients as an attachment delivers 12.5 GB of mail; as a hosted link it delivers ~0.

POST /v3/uploads

Send the request as multipart/form-data (not JSON):

bashUpload with cURL
curl -X POST https://api.wemail.io/v3/uploads \
  -H "Authorization: Bearer $WEMAIL_API_KEY" \
  -F "file=@/path/to/report.pdf" \
  -F "expires_at=2027-01-01T00:00:00Z"
ParameterTypeRequiredDescription
filefile partRequiredThe file to host, in a multipart part named file. Up to 25 MB per file.
expires_atstring (ISO 8601)OptionalAuto-expire the file at this timestamp. Expired links answer 410 Gone and the bytes stop counting toward your quota. Omit for no expiry.

Uploading a name that already exists never overwrites: the new file is kept apart automatically — a second report.pdf becomes report (2).pdf, a third report (3).pdf (the response returns the final name). Deleting a file frees its name again.

The hosted URL

The returned url (e.g. https://track.wemail.io/f/up_9f3KzQ7w/report.pdf) is stable and reusable across sends. It is served from the tracking host, so when it appears in a message body it is wrapped and tracked like any other link — downloads register as click events, per recipient — and branded tracking domains apply to it automatically. Each download also increments the upload's download_count. Under the hood the link answers with a short-lived redirect to private storage; the storage itself is never publicly addressable.

htmlA download button in your email HTML
<a href="https://track.wemail.io/f/up_9f3KzQ7w/report.pdf"
   style="display:inline-block;padding:12px 24px;background:#16a34a;color:#fff;
          border-radius:6px;text-decoration:none;font-weight:600">
  Download the report (PDF)
</a>

Limits

ParameterTypeRequiredDescription
Per filelimitOptional25 MB per uploaded file. Larger files are rejected with 413.
PlanlimitOptionalHosted uploads are a paid-plan feature (Pay-as-you-go counts as paid) — requests from free plans are rejected with 403.
Your domainlimitOptionalA verified sending domain with the branded tracking CNAME (track.yourdomain.com) is required — hosted files are served from *your* domain, so link reputation is yours and matches your From domain (a deliverability plus). Without an active tracking CNAME the feature is locked (403). If a hosted file is ever flagged by security vendors, it is your own domain at stake.
Storage quotalimitOptionalPaid plans include 100 MB; storage add-ons raise it to 10 GB or 100 GB. Check usage with GET /v3/uploads; delete unused files to free space.
File typeslimitOptionalExecutables, scripts, markup (.html, .svg), package and disk-image formats are rejected with 400.
ArchiveslimitOptional.zip is the only accepted archive format, and its file list is security-inspected without extraction: archives containing executables/scripts or nested archives are rejected. Other archive formats (.rar, .7z, .gz, …) are refused because they cannot be inspected.

Responses

StatusDescription
201Created. The full upload object, including the hosted url to embed in your emails.
400Request body is not multipart/form-data (e.g. JSON), missing file part, blocked file type, storage quota exceeded, empty file, or invalid expires_at.
403Hosted uploads require a paid plan (Pay-as-you-go counts as paid).
413The file exceeds the 25 MB per-file limit.
503Hosted uploads are not configured on this deployment.

Code examples

cURL
curl -X POST https://api.wemail.io/v3/uploads \
  -H "Authorization: Bearer afn_live_pK7…b9aF" \
  -F "file=@report-2026.pdf" \
  -F "expires_at=2026-12-31T23:59:59Z"
Node.js
import { readFileSync } from "node:fs";
import { WemailClient } from "@wemail/sdk";
const wemail = new WemailClient(process.env.WEMAIL_API_KEY);

const file = new Blob([readFileSync("report-2026.pdf")], { type: "application/pdf" });
const upload = await wemail.createUpload(file);
// Put upload.url on a link or button in your email HTML.
console.log(upload.url); // https://track.wemail.io/f/up_9f3KzQ7w/report-2026.pdf
Python
from wemail.client import WemailClient

wemail = WemailClient(api_key=os.environ["WEMAIL_API_KEY"])

with open("report-2026.pdf", "rb") as f:
    upload = wemail.create_upload(("report-2026.pdf", f, "application/pdf"))
print(upload.url)  # put this on a link or button in your email HTML
PHP
<?php
$wemail = new \Wemail\WemailClient(getenv('WEMAIL_API_KEY'));
$upload = $wemail->createUpload('report-2026.pdf');
echo $upload->getUrl(); // put this on a link or button in your email HTML
Ruby
wemail = Wemail::Client.new(api_key: ENV["WEMAIL_API_KEY"])
upload = wemail.create_upload(File.open("report-2026.pdf", "rb"))
puts upload.url # put this on a link or button in your email HTML
Go
client, err := wemail.NewClient(os.Getenv("WEMAIL_API_KEY"))
file, err := os.Open("report-2026.pdf")
if err != nil { log.Fatal(err) }
defer file.Close()
upload, err := client.CreateUpload(file)
fmt.Println(upload.GetUrl()) // put this on a link or button in your email HTML
Java
WemailClient wemail = new WemailClient(System.getenv("WEMAIL_API_KEY"));
Upload upload = wemail.createUpload(new File("report-2026.pdf"));
System.out.println(upload.getUrl()); // put this on a link or button
.NET
var wemail = new WemailClient(Environment.GetEnvironmentVariable("WEMAIL_API_KEY"));
var upload = await wemail.CreateUploadAsync(
  new FileParameter("report-2026.pdf", "application/pdf", File.OpenRead("report-2026.pdf")));
Console.WriteLine(upload.Url); // put this on a link or button
ResponseExample response
{
  "id": "up_9f3KzQ7wXt2mB4hN",
  "filename": "report-2026.pdf",
  "content_type": "application/pdf",
  "size_bytes": 2621440,
  "url": "https://track.wemail.io/f/up_9f3KzQ7wXt2mB4hN/report-2026.pdf",
  "download_count": 0,
  "expires_at": "2026-12-31T23:59:59.000Z",
  "created_at": "2026-08-25T10:14:02.000Z"
}