How to Send a Base64 Image with the Telegram Bot API
You have an image as a Base64 string — from an API response, a webhook payload, or a converter — and you try to pass it straight into sendPhoto as data:image/png;base64,.... Telegram answers 400 Bad Request, and if you stuffed the string into a URL you may have seen 414 Request-URI Too Large instead. That is not a bug in your code. The Telegram Bot API does not accept Base64 or data URIs for photos at all — and it never has. Here is what it does accept, and the decode-then-upload fix in Node.js, Python and curl.
The short answer
The photo parameter of sendPhoto accepts exactly three things:
- A multipart/form-data file upload — the bytes of the image, posted like a normal file form field.
- A public HTTPS URL — Telegram's servers fetch the image themselves.
- A
file_id— Telegram's handle for a file that was already uploaded to Telegram (for example a photo a user sent your bot).
A Base64 string is none of the three, and a data: URI is not a fetchable URL. The fix: strip the data:image/...;base64, prefix, decode the payload to bytes, and upload it as multipart/form-data.
Why sendPhoto rejects Base64
Telegram's servers never see your string as an image. When you pass a URL, they download it; when you pass a file, the HTTP client packs the raw bytes into a multipart body. Base64 is a text encoding sitting in front of both paths:
- A data URI is not a URL Telegram can fetch — there is no host, and the
data:scheme is explicitly not fetchable. - A raw Base64 string is text, not bytes — even without the prefix, Telegram stores it as a broken text "document" or rejects the request.
- If the data URI lands in the request URL or query string, the request usually dies before reaching the API with HTTP 414 — Base64 payloads run thousands of characters and blow past every URL length limit.
This trips up bot builders on every stack because vision APIs like OpenAI do accept data URIs — see the payload shapes for AI vision APIs. Chat APIs have the opposite rule: bytes or a URL, never a data URI.
Working code: decode, then upload
The decode step is the same everywhere: cut everything up to the first comma, then Base64-decode the rest. The upload is a normal multipart POST.
Node.js (node-telegram-bot-api)
const b64 = 'data:image/png;base64,iVBORw0KGgoAAAANS...';
// strip the data: prefix, decode to binary
const raw = Buffer.from(b64.replace(/^data:image\/[a-z+]+;base64,/, ''), 'base64');
// the library streams a Buffer as multipart/form-data automatically
await bot.sendPhoto(chatId, raw, { caption: 'Generated report' });
Node.js (raw fetch, no library)
const raw = Buffer.from(b64.split(',')[1], 'base64');
const form = new FormData();
form.append('chat_id', String(chatId));
form.append('photo', new Blob([raw], { type: 'image/png' }), 'photo.png');
await fetch(`https://api.telegram.org/bot${TOKEN}/sendPhoto`, {
method: 'POST',
body: form,
});
Python (requests)
import base64, requests
raw = base64.b64decode(b64.split(',', 1)[1]) # drop the data: prefix, decode
r = requests.post(
f"https://api.telegram.org/bot{TOKEN}/sendPhoto",
data={"chat_id": chat_id},
files={"photo": ("photo.png", raw, "image/png")},
)
r.raise_for_status()
curl
# 1. strip the prefix (if present) and decode to a file
sed 's/^data:image\/[a-z+]*;base64,//' image.b64 | base64 -d > photo.png
# 2. upload it as multipart
curl -s "https://api.telegram.org/bot$BOT_TOKEN/sendPhoto" \
-F chat_id="123456789" \
-F "photo=@photo.png;type=image/png"
If your string is already raw Base64 with no data: prefix, skip the sed step. Wrapped lines (the 76-character line breaks some encoders insert) are handled by base64 -d and by the decoders in the examples above.
Size limits and gotchas
- 10 MB after decoding. Photos must be at most 10 MB — and remember the Base64 string is about 33% larger than the bytes it represents, so a 13 MB string is already too big. Encode smaller images with the Image to Base64 converter or shrink photos first via compress JPG to Base64.
- sendPhoto re-encodes. Telegram converts photos for in-chat display, which can flatten PNG transparency and reduce quality. If the exact bytes matter (logos, screenshots, certificates), send them with
sendDocumentinstead — same multipart fix, different method name. - Keep the prefix decision straight. The
data:prefix is only meaningful inside HTML/CSS/JSON renderers. Upload APIs want either the raw payload or the decoded bytes — the invalid Base64 troubleshooting guide covers the prefix bugs that survive into requests. - GIF animation needs sendAnimation. An animated GIF posted via
sendPhotoarrives as a static frame.
URL and file_id alternatives
Sometimes you can skip the upload entirely:
- Public HTTPS URL. If the image already lives at a reachable URL, pass it directly — Telegram downloads it server-side. The URL must be publicly reachable; localhost, intranet hosts and signed URLs that expire mid-flight will fail.
- file_id roundtrip. Everything Telegram ever received — including photos users send your bot — has a stable
file_id. Re-sending byfile_idis instant and skips uploads completely, which makes it the cheapest option for repeat sends of the same image.
Frequently asked questions
Can the Telegram Bot API accept a Base64 or data URI image directly?
No. sendPhoto only accepts a file_id, a public HTTPS URL, or a multipart/form-data upload. A Base64 string or data URI is none of these, so Telegram answers 400 Bad Request.
Why do I get HTTP 414 when sending a Base64 image to Telegram?
The data URI was stuffed into the request URL or query string. Base64 payloads run thousands of characters and do not fit in a URL — decode the payload and send it as a multipart file upload instead.
What is the maximum photo size for sendPhoto?
Up to 10 MB after decoding. Base64 adds about 33%, so keep an eye on the string length too — the size calculator shows the overhead before you send.
Should I use sendPhoto or sendDocument?
sendPhoto shows an inline preview but Telegram may re-encode the image. sendDocument preserves the original bytes (good for PNG transparency) without the preview.
Can I pass an image URL instead of uploading?
Yes — any publicly reachable http(s) URL works, since Telegram fetches it server-side. A data: URI is not a fetchable URL, which is exactly why it fails. A previously seen file_id also works.
How do I strip the data: prefix before decoding?
Remove everything up to and including the first comma: b64.split(',')[1] in JavaScript, b64.split(',', 1)[1] in Python, or the sed one-liner above in shell. The prefix belongs to browser rendering, not to upload APIs.
Building a pipeline that also feeds images to AI vision APIs? Those want the opposite format — a full data URI in JSON. The Image to Base64 for API page outputs both payload shapes, and the homepage converter produces plain data URIs for HTML, CSS and email.
Posting to other chat platforms instead? Slack answers no_file_data for the same reason — the Slack Base64 fix walks through the three-call upload flow — and discord.js needs the string decoded to a Buffer first, per the discord.js Base64 guide.