Send Files via Discord Webhook (Images, Attachments & Multipart)
Upload files through Discord webhooks using multipart/form-data. Working examples in Python, JavaScript, curl and PHP — covers images, PDFs, logs and embed attachments.
Discord webhooks aren’t just for text. You can upload screenshots, log files, PDFs, archives — anything up to 25 MB (Discord’s 2026 free-tier file limit). The catch: you must switch from JSON to multipart/form-data. Get the format wrong and Discord rejects the upload silently with a generic 400.
This guide shows the exact request shape and working code for Python, JavaScript, curl and PHP — including how to attach a file inside an embed image.
How File Uploads Work
Normally you POST application/json:
{ "content": "Hello" }
For files, you POST multipart/form-data with two parts:
payload_json— your usual webhook JSON (content, embeds, username, etc.)files[N]— the binary file content, indexed from0
That’s it. The same JSON you’d send normally goes inside payload_json; the file rides alongside it.
File Limits (2026)
| Tier | Max single file | Max files per message |
|---|---|---|
| Free | 25 MB | 10 |
| Nitro Basic | 50 MB | 10 |
| Nitro | 500 MB | 10 |
| Boost (Server) | 50–100 MB based on level | 10 |
Webhooks use the server’s boost tier when sending. So a webhook in a Level 3 server can send 100 MB files even from a free account.
Python — requests
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN"
with open("screenshot.png", "rb") as f:
files = {
"file": ("screenshot.png", f, "image/png"),
}
payload = {
"payload_json": '{"content": "Build artifact:", "username": "CI Bot"}'
}
r = requests.post(WEBHOOK_URL, data=payload, files=files)
r.raise_for_status()
Key points:
payload_jsonis a string (not a dict) insidedata- The MIME type matters — use
image/png,application/pdf,text/plain, etc. requestsauto-builds the multipart boundary
Multiple Files
files = {
"files[0]": ("error.log", open("error.log", "rb"), "text/plain"),
"files[1]": ("trace.txt", open("trace.txt", "rb"), "text/plain"),
"files[2]": ("dump.bin", open("dump.bin", "rb"), "application/octet-stream"),
}
requests.post(WEBHOOK_URL, data={"payload_json": '{"content": "Crash report"}'}, files=files)
The keys must be files[0], files[1], … in order.
JavaScript — Node.js FormData
Modern Node 18+ has built-in FormData and fetch:
import fs from 'node:fs';
const form = new FormData();
form.append('payload_json', JSON.stringify({
content: 'Daily report attached',
username: 'Reports Bot',
}));
const buffer = fs.readFileSync('./report.pdf');
form.append('file', new Blob([buffer], { type: 'application/pdf' }), 'report.pdf');
const res = await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
body: form,
});
console.log(res.status);
Don’t set Content-Type manually — fetch derives the multipart boundary from the FormData instance.
Browser Upload (No Backend)
If you’re letting users upload directly from a <input type="file">:
async function uploadToDiscord(file, message) {
const form = new FormData();
form.append('payload_json', JSON.stringify({ content: message }));
form.append('file', file, file.name);
const res = await fetch(WEBHOOK_URL, { method: 'POST', body: form });
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
}
document.querySelector('input[type=file]').addEventListener('change', async (e) => {
await uploadToDiscord(e.target.files[0], 'New upload from website');
});
Warning: never expose your webhook URL in client-side code unless you’re fine with anyone using it. Proxy through your backend in production.
curl — Command Line
curl -X POST \
-F "payload_json={\"content\":\"Log file attached\"}" \
-F "file=@./server.log;type=text/plain" \
https://discord.com/api/webhooks/ID/TOKEN
Quoting matters: the JSON inside payload_json must escape quotes if you’re in a shell that doesn’t strip them. If your JSON is complex, write it to a temp file:
echo '{"content":"Log","embeds":[{"title":"Build #42","color":3066993}]}' > /tmp/p.json
curl -X POST \
-F "payload_json=<@/tmp/p.json" \
-F "file=@./build.log" \
https://discord.com/api/webhooks/ID/TOKEN
PHP — CURLFile
<?php
$webhook = 'https://discord.com/api/webhooks/ID/TOKEN';
$payload = json_encode([
'content' => 'Daily backup attached',
'username' => 'Backup Bot',
]);
$ch = curl_init($webhook);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'payload_json' => $payload,
'file' => new CURLFile('/srv/backups/db.sql.gz', 'application/gzip', 'db.sql.gz'),
]);
$response = curl_exec($ch);
curl_close($ch);
CURLFile automatically sets the right MIME boundary; never use @filename strings (deprecated and insecure since PHP 5.6).
Embed Images via Attachment
Want the file to appear inside an embed instead of as a separate attachment? Reference it with attachment://:
import requests
with open("chart.png", "rb") as f:
files = {"file": ("chart.png", f, "image/png")}
payload_json = '''
{
"embeds": [{
"title": "Q4 Revenue",
"image": { "url": "attachment://chart.png" }
}]
}
'''
requests.post(WEBHOOK_URL, data={"payload_json": payload_json}, files=files)
The filename in attachment://chart.png must match the filename in the multipart part exactly. This works for image, thumbnail, author.icon_url, and footer.icon_url.
Streaming Large Files
For files near the 25 MB cap, stream from disk to avoid loading the whole thing into memory:
import requests
def upload_large(path: str, message: str):
with open(path, "rb") as f:
# requests streams automatically when given a file object
files = {"file": (path.split("/")[-1], f)}
return requests.post(
WEBHOOK_URL,
data={"payload_json": f'{{"content": "{message}"}}'},
files=files,
timeout=60, # bigger timeout for big uploads
)
upload_large("/var/log/app/2026-04.tar.gz", "Monthly logs")
Spoiler-Tagged Files
Prefix the filename with SPOILER_ to mark a file as a spoiler (Discord blurs it):
files = {"file": ("SPOILER_screenshot.png", open("screenshot.png", "rb"), "image/png")}
Useful for security-sensitive screenshots that should still be inspectable but not auto-displayed.
Common Errors
| Status | Cause | Fix |
|---|---|---|
400 + Request entity too large | File over tier limit | Compress, split, or use server boost |
400 + payload_json error | Invalid JSON inside payload_json | Validate with json.dumps() first |
| 413 | Server-level size limit | Same as 400 — reduce file size |
| 415 | Wrong content type | Don’t set Content-Type: application/json for file uploads |
| 200 but no file appears | Filename mismatch with attachment:// | Match filename exactly, including extension |
Try It Live
The Discord Webhook builder lets you drag-drop files into a message and previews exactly how the upload will look in Discord — same multipart format described here. For other webhook recipes, see the Python guide and JavaScript guide.
References
- Discord docs: Uploading Files
- Discord docs: Execute Webhook
Try it in our tool
Open Discord Webhook Builder