Как отправить фото и файлы через WhatsApp API: счета, PDF, картинки
Счёт на оплату, фото товара, акт сверки, схема проезда — то, что удобнее отправить файлом, чем описывать словами. В wapi.kz вложения уходят тем же запросом POST /messages/send, что и текст: вы передаёте ссылку на файл, мы скачиваем его и отправляем в WhatsApp.
Ниже — поля запроса, требования к ссылке, как отдать файл из закрытого хранилища, ошибки и примеры отправки счёта PDF из Node.js и Python. Основы — ключ, from, коды ошибок — в гайде про отправку сообщений через API, а готовые сценарии вроде счёта после заказа — в гайде про уведомления клиентам.
Как это устроено
- Файл лежит там, откуда его можно скачать по ссылке: на вашем сервере или в хранилище.
- Вы отправляете
POST /messages/sendс полемimage,file,voiceилиvideoи этой ссылкой. - Сервер wapi.kz скачивает файл и отправляет получателю. Своего лимита размера нет — действует лимит WhatsApp, до 2 ГБ на файл.
- В ответе — те же
idиwa_message_id, что у текста; в журнале у фото типimage, у файла —document, у голосового —voice, у видео —video. Отправленный файл сохраняется в хранилище и появляется в полеmediaсообщения.
Загружать файл в wapi.kz заранее не нужно, base64 в теле запроса тоже не передаётся — только ссылка.
Фото: поле image
curl -X POST https://wapi.kz/api/v1/messages/send \
-H "Authorization: Bearer $WAPI_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"77011234567","from":1,"image":{"url":"https://shop.example.kz/img/sofa-grey.jpg","caption":"Диван «Алматы», серый — есть в наличии"}}'Поддерживаются JPEG, PNG и WebP — получатель увидит картинку прямо в чате. Тип определяется по заголовку Content-Type, который отдаёт сервер с файлом, а не по расширению в ссылке. Если сервер отдаёт картинку как application/octet-stream, API ответит invalid_media — поправьте заголовок или отправьте файл через поле file. Так же, документом, уходят GIF, HEIC и другие форматы.
Документ: поле file
{
"to": "77011234567",
"from": 1,
"file": {
"url": "https://files.example.kz/invoices/1209.pdf?exp=1792160000&sig=9f2c41d7",
"filename": "Счёт №1209.pdf",
"caption": "Счёт №1209 на оплату"
}
}file принимает любой тип: PDF, таблицы, документы Word, архивы. Получатель увидит документ с именем и размером и откроет его на телефоне.
Имя файла
filename — то, что увидит клиент. Кириллица, пробелы и «№» — пожалуйста: «Счёт №1209.pdf» придёт именно так, JSON передаётся в UTF-8. Если имя не указать, мы возьмём последнюю часть пути из ссылки (1209.pdf), а если её нет — назовём файл file. Не забывайте расширение: без него получателю сложнее открыть документ. Имя длиннее 120 символов обрежется.
Подпись
caption необязателен. Если его нет, подписью станет поле text из того же запроса; нет ни того, ни другого — файл уйдёт без подписи. Длина подписи — до 4096 символов, как у обычного текста. Отдельное сообщение перед файлом не нужно: подпись и файл приходят одним сообщением.
Голосовое: поле voice
{ "to": "77011234567", "from": 1, "voice": { "url": "https://files.example.kz/audio/reply.mp3" } }Подойдут MP3, M4A, WAV, OGG, AAC и AMR: мы перекодируем запись в голосовое WhatsApp (OGG/Opus с волной и плеером), и клиент увидит его так же, как записанное на телефоне. Подписи у голосового нет — нужный текст отправьте отдельным сообщением.
Видео: поле video
{ "to": "77011234567", "from": 1, "video": { "url": "https://files.example.kz/video/obzor.mov", "caption": "Обзор дивана «Алматы»" } }MP4 уходит как есть, MOV, WebM и 3GP перекодируем в MP4 (H.264). Подпись caption работает так же, как у фото. Крупное видео скачивается и перекодируется дольше — ответ может прийти через десятки секунд, поэтому дайте запросу время.
Требования к ссылке
- Публичная: открывается без логина, cookies и заголовков авторизации.
httpилиhttps, адрес в интернете — ссылки наlocalhostи внутреннюю сеть не скачиваются.- Не больше 3 перенаправлений.
- Файл — до 2 ГБ: это лимит самого WhatsApp, своего ограничения у нас нет.
- Сервер отвечает кодом 200 и отдаёт сам файл, а не страницу «Скачать».
Последний пункт — частая ловушка с облачными дисками и файлообменниками: ссылка «поделиться» ведёт на HTML-страницу с кнопкой, а не на файл. Нужна прямая ссылка на скачивание. Проверить, что ссылка без авторизации отдаёт сам файл, можно одной командой:
curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}\n" "https://files.example.kz/invoices/1209.pdf?exp=1792160000&sig=9f2c41d7"
# 200 application/pdf 184213 — всё в порядкеВременная ссылка из закрытого хранилища
Счета и договоры не стоит класть в открытую папку навсегда. Решение — временная ссылка, которая работает ограниченное время и потом перестаёт открываться:
- В S3-совместимых хранилищах это подписанная ссылка (presigned URL) на скачивание: её выдаёт SDK хранилища, срок жизни задаётся в секундах.
- Если файлы лежат на вашем сервере, подпишите ссылку сами: имя файла и срок действия плюс HMAC с секретом. Так устроен пример на Node.js ниже.
Ставьте срок с запасом — хотя бы 10–15 минут, чтобы ссылка не истекла, пока сообщение ждёт в очереди. Клиент ссылку не увидит: мы скачиваем файл в момент отправки, и в WhatsApp приходит сам документ.
Ошибки вложений
| code | Когда | Что делать |
|---|---|---|
invalid_media | Не ссылка, адрес во внутренней сети, файл пустой, в image не JPEG, PNG или WebP, в voice не аудио, в video не видео | Проверить ссылку и Content-Type; не картинку отправить через file |
media_too_large | Файл больше 2 ГБ — WhatsApp такой не примет | Сжать файл или отправить ссылку на него текстом |
media_fetch_failed | Не скачался: адрес не открывается, ответ не 200, больше 3 перенаправлений, ссылка истекла или требует входа | Проверить ссылку командой выше; если истекла — выпустить новую |
busy | Сервер прямо сейчас принимает много крупных файлов | Повторить через несколько минут |
Первые три приходят с HTTP 400, busy — с HTTP 503: сообщение не отправляется и в журнал не попадает. Остальные ошибки — rate_limited, not_connected, number_paused и другие — такие же, как у текста, см. документацию по ошибкам.
Пример: счёт PDF из Node.js
Счета лежат на вашем сервере в /var/invoices. Express отдаёт их по временной подписанной ссылке, функция sendInvoice отправляет счёт клиенту. Node.js 18+, пакет express.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.FILE_LINK_SECRET; // длинная случайная строка
const PUBLIC_URL = "https://shop.example.kz"; // адрес этого сервера снаружи
const sign = (name, exp) => crypto.createHmac("sha256", SECRET).update(`${name}:${exp}`).digest("hex");
function signedUrl(name, ttl = 3600) {
const exp = Math.floor(Date.now() / 1000) + ttl;
return `${PUBLIC_URL}/files/${encodeURIComponent(name)}?exp=${exp}&sig=${sign(name, exp)}`;
}
const app = express();
app.get("/files/:name", (req, res) => {
const { exp, sig } = req.query;
if (sig !== sign(req.params.name, exp) || Number(exp) < Date.now() / 1000) return res.sendStatus(403);
res.sendFile(req.params.name, { root: "/var/invoices" });
});
app.listen(3000);
export async function sendInvoice(to, invoiceNo) {
const res = await fetch("https://wapi.kz/api/v1/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to,
from: 1,
file: {
url: signedUrl(`${invoiceNo}.pdf`),
filename: `Счёт №${invoiceNo}.pdf`,
caption: `Счёт №${invoiceNo} на оплату. Вопросы — в ответ на это сообщение.`,
},
}),
signal: AbortSignal.timeout(60000), // мы сначала скачиваем файл — дайте запросу время
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
return data.id; // по нему GET /messages/{id} покажет статус доставки
}Пример: счёт PDF из Python
Ссылку на PDF уже выдало ваше хранилище или учётная система. Функция повторяет отправку при rate_limited и понятно объясняет ошибки вложения.
import os
import time
import requests
API = "https://wapi.kz/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WAPI_KEY']}"}
def send_invoice(to, invoice_no, pdf_url, from_number=1):
body = {
"to": to,
"from": from_number,
"file": {
"url": pdf_url,
"filename": f"Счёт №{invoice_no}.pdf",
"caption": f"Счёт №{invoice_no} на оплату",
},
}
for _ in range(3):
r = requests.post(f"{API}/messages/send", headers=HEADERS, json=body, timeout=60)
data = r.json()
if r.status_code == 200:
return data["id"]
code = data.get("code")
if code == "rate_limited":
time.sleep(2)
continue
if code == "media_too_large":
raise ValueError("Файл больше 2 ГБ: WhatsApp такой не примет — отправьте ссылку текстом")
if code in ("invalid_media", "media_fetch_failed"):
raise ValueError(f"Ссылка не подошла: {data['error']}")
raise RuntimeError(f"{r.status_code} {code}: {data['error']}")
raise RuntimeError("Номер занят другими отправками, повторите позже")
url = "https://files.example.kz/invoices/1209.pdf?exp=1792160000&sig=9f2c41d7"
print("id в журнале:", send_invoice("77011234567", 1209, url))Через MCP: файл отправляет ИИ-агент
Инструмент send_message MCP-сервера принимает image_url, file_url с filename, voice_url или video_url; text при вложении становится подписью. Агенту достаточно сказать обычными словами: «Отправь клиенту 77011234567 счёт по этой ссылке с подписью „Счёт №1209 на оплату“» — он сам вызовет инструмент примерно так:
{
"name": "send_message",
"arguments": {
"to": "77011234567",
"text": "Счёт №1209 на оплату",
"file_url": "https://files.example.kz/invoices/1209.pdf?exp=1792160000&sig=9f2c41d7",
"filename": "Счёт №1209.pdf"
}
}Требования к ссылке те же. Как подключить MCP-сервер — в гайде про ИИ-агентов и MCP и в документации.
Чего пока нет
- Вложения, как и текст, уходят только в личные чаты, не в группы.
- Одно сообщение — одно вложение, у голосового нет подписи.
Входящие файлы от клиентов сохраняются сами: в вебхуке приходит hasMedia: true, а через несколько секунд файл появляется в поле media сообщения — ссылка на скачивание, см. документацию.
Подключить номер можно после регистрации. Номер стоит 9 900 ₸ в месяц, за отправку фото и файлов отдельно не платите. Отправленные и полученные файлы хранятся: 5 ГБ на аккаунт бесплатно, сверх — 1 000 ₸ в месяц за каждые 5 ГБ, см. цены.
Частые вопросы
Можно ли отправить файл без ссылки, прямо из программы?
Нет, только по ссылке: загрузки файла в теле запроса нет. Положите файл на свой сервер или в хранилище и передайте временную ссылку.
Какой максимальный размер файла?
Своего лимита у нас нет — действует лимит WhatsApp: до 2 ГБ на файл. Файл больше отклоняется с кодом media_too_large.
Можно ли отправить несколько файлов одним сообщением?
Нет: одно сообщение — одно вложение. Несколько файлов — несколько запросов, с учётом лимита 1 сообщение в секунду с номера.
Увидит ли клиент мою ссылку на файл?
Нет. Мы скачиваем файл и отправляем его как вложение — клиент получает сам документ или фото, а ссылка нигде не показывается.
Как отправить голосовое сообщение или видео?
Полем voice или video с публичной ссылкой. MP3, M4A, WAV, OGG, AAC и AMR мы перекодируем в голосовое WhatsApp, MP4 уходит как есть, MOV, WebM и 3GP перекодируем в MP4. Подробнее — в документации.
Подключите WhatsApp к своим системам
9 900 ₸ в месяц за номер, без платы за сообщения. Сервер в Казахстане, оплата Kaspi или счётом на ТОО/ИП. Подключение — около 5 минут.