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.

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.