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.