Discord Bot File Storage
Discord File Hosting for Bots: Permanent Image and Attachment URLs
Discord file hosting for bots: permanent CDN URLs for images, embeds, and attachments.
Discord bot file storage
Discord's attachment CDN was built to render chat messages, not to back a bot's data layer. Since 2024, cdn.discordapp.com links carry signed expiry parameters (ex, is, hm), so an attachment URL you saved in a database eventually returns a 404 once the signature times out. If your bot persists user avatars, generated images, moderation evidence, or ticket transcripts by stashing the raw Discord URL, those references rot. Re-hosting the bytes on storage you control fixes that permanently.
Two jobs share one upload path here. The first is Discord file hosting for assets your bot emits: renders, charts, transcripts, generated audio. The second is the moment Discord refuses the file outright. A single message is capped at 10 MB on the free tier and up to 100 MB on a boosted server, so a bot that can't upload images past that ceiling should stop fighting the limit and post a link instead.
A public cdn22.net URL resolves straight to the bytes, Discord unfurls it in the channel, and the user downloads from the nearest edge rather than through Discord's transfer path.
Re-hosting an incoming attachment is the same three calls. Fetch the bytes from message.attachments.first().url while the signature is still valid, push them through the upload, and persist the returned CDN URL instead of Discord's. That URL carries no expiry token, so it can sit in your database for years and still resolve.
client.on('messageCreate', async (msg) => {
const att = msg.attachments.first();
if (!att) return;
const bytes = Buffer.from(await (await fetch(att.url)).arrayBuffer());
// 1) POST /v1/files/{folderId} -> { urls: [{ url, id }] }
const meta = await api.post(`/v1/files/${FOLDER}`, {
filesMetadata: [{ name: att.name }],
});
const { url, id } = meta.urls[0];
// 2) PUT the bytes straight to the presigned URL
await fetch(url, { method: 'PUT', body: bytes });
// 3) confirm to finalize
await api.post('/v1/files/confirm-upload', { ids: [id] });
msg.reply(`Stored permanently: ${cdnUrlFor(id)}`);
});Nothing about this is Node-specific. The same three calls run from a shell, so a discord.py bot, a Go worker, or a deploy script can host images for a Discord bot with curl and no SDK. Read the key from the environment; never paste it into source or a slash-command handler.
# 1) reserve a slot -> presigned PUT url + file id
META=$(curl -s -X POST "https://api.cdn22.net/v1/files/$FOLDER_ID" \
-H "Authorization: $CDN22_API_KEY" \
-H "Content-Type: application/json" \
-d '{"filesMetadata":[{"name":"rank-card.png"}]}')
PUT_URL=$(echo "$META" | jq -r '.urls[0].url')
FILE_ID=$(echo "$META" | jq -r '.urls[0].id')
# 2) upload the bytes, 3) confirm
curl -s -X PUT --upload-file rank-card.png "$PUT_URL"
curl -s -X POST https://api.cdn22.net/v1/files/confirm-upload \
-H "Authorization: $CDN22_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"ids\":[\"$FILE_ID\"]}"The Authorization header takes your raw API key with no Bearer prefix, and every endpoint is browsable in the hosted Swagger UI at api.cdn22.net/api-docs. The curl walkthrough covers the same three calls line by line, and the image upload API covers the image-specific path.
Embeds want a public URL. Set image.url or thumbnail.url on the embed to a permanent cdn22.net link and Discord fetches it through its own proxy, which means the URL has to still work whenever Discord refetches it. That rules out short-lived signed links in embed fields: a signed URL is the right tool for a moderation export or a paid download your backend hands to one person, not for artwork the whole channel renders.
Mark a file private at upload and no public URL is minted at all; your bot calls GET /v1/files/signed/{id} for a link that expires in ten minutes by default. Secure file sharing goes deeper on the signed-link mechanics, and permanent file links on the public side.
There is no format gate. PNG, JPEG, WebP, GIF, SVG, MP4, WAV, JSON, and log files all upload through the same endpoints and come back on a CDN URL, so image storage for rank cards and a 300 MB archived render use one bucket of credits rather than two vendors. Delivery runs on a global edge network with no distribution to configure, which is the part covered on file storage with CDN.
Give each bot its own project and API key. Revoking a leaked key then takes one bot offline instead of your whole fleet, and usage reads per project when you want to know which bot is actually spending. Billing is prepaid credits: storage, delivery, and requests draw from one balance, so a channel that goes viral spends credits you can watch rather than producing an invoice three weeks later. Create a project and upload one bot asset to see the URL you would put in an embed.
Bot asset workflow
From Bot Asset to Embed URL
One upload path covers a generated render, a re-hosted Discord attachment, and a private export your bot hands to a single moderator.
Create a project and API key
One project per bot, one key per project, stored in the bot's environment. Revoking a leaked key then only affects that bot. Start in the app.
Upload from the bot or the dashboard
Three calls: POST /v1/files/{folderId} for a presigned PUT target and id, PUT the bytes, POST /v1/files/confirm-upload. Drag and drop works too for artwork you ship with the bot. The upload a file guide walks the first one.
Put the CDN URL in the embed
A public file returns a direct link the embed's image field can use, so Discord renders it from the nearest edge. The same URL works in a slash-command reply, a webhook payload, or your dashboard. See permanent file links.
Keep private assets signed
Moderation evidence and ticket exports upload private, so no public URL exists. Call GET /v1/files/signed/{id} for a link that expires in ten minutes, and mint a fresh one per request instead of storing it. More on secure file sharing.
Before you ship the bot
- Read the API key from an environment variable, never from source or a command handler.
- Re-host an incoming attachment while its signature is still valid, then store your URL.
- Use public URLs in embed image fields; Discord refetches them and a signed link will 404.
- Uploading needs a cdn22.net account; the people clicking your links never need one.
- Credits cover storage, delivery, and requests from one balance, so watch a viral channel's spend.
Benefits With No Complexity
Global CDN delivery
Edge-cached worldwide
Signed-URL security
What You Get
Unlimited files
Unlimited storage
Public + Private storage
CDN ready links
Prepaid credits
More coming soon
How cdn22.net Works
1. Upload
2. Copy
3. Use Anywhere
Why Developers Choose cdn22.net
A Better Way to Store & Deliver Files
| Assets in your repo | S3 or R2 alone | Ad hoc image hosts | cdn22.net | |
|---|---|---|---|---|
| Permanent URLs with no expiry signature | ||||
| Upload at runtime with no redeploy | ||||
| Global CDN delivery built in | ||||
| Private files with signed URLs | ||||
| No IAM policy, bucket policy, or CORS setup | ||||
| Storage and delivery on one prepaid balance |
Calculate Your Needs
Storage
Egress
CDN Bandwidth
Need storage, egress, and request fees broken out — and compared against AWS list price for the same bytes? Use the CDN & storage cost calculator.
Start Discord Bot Storage
- Global CDN delivery
- Edge-cached worldwide
- Signed-URL security
- Unlimited files
- Unlimited storage
- Public + Private storage
- No subscription — prepaid credits keep spend predictable