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):
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"| Parameter | Type | Required | Description |
|---|---|---|---|
file | file part | Required | The file to host, in a multipart part named file. Up to 25 MB per file. |
expires_at | string (ISO 8601) | Optional | Auto-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.
<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
| Parameter | Type | Required | Description |
|---|---|---|---|
Per file | limit | Optional | 25 MB per uploaded file. Larger files are rejected with 413. |
Plan | limit | Optional | Hosted uploads are a paid-plan feature (Pay-as-you-go counts as paid) — requests from free plans are rejected with 403. |
Your domain | limit | Optional | A 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 quota | limit | Optional | Paid 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 types | limit | Optional | Executables, scripts, markup (.html, .svg), package and disk-image formats are rejected with 400. |
Archives | limit | Optional | .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
| Status | Description |
|---|---|
201 | Created. The full upload object, including the hosted url to embed in your emails. |
400 | Request body is not multipart/form-data (e.g. JSON), missing file part, blocked file type, storage quota exceeded, empty file, or invalid expires_at. |
403 | Hosted uploads require a paid plan (Pay-as-you-go counts as paid). |
413 | The file exceeds the 25 MB per-file limit. |
503 | Hosted uploads are not configured on this deployment. |
Code examples
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"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.pdffrom 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
$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 HTMLwemail = 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 HTMLclient, 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 HTMLWemailClient 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 buttonvar 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{
"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"
}