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 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:

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

URL and file_id alternatives

Sometimes you can skip the upload entirely:

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.