Перейти к содержимому

Надёжная отправка без дублей

Обновлено 17 октября 2026

Сеть рвётся, тайм-ауты случаются. Чтобы повтор запроса не превратился во второе сообщение клиенту, передавайте в отправке свой ключ — clientMessageId.

Где работает

sendMessage, sendFileByUpload, sendFileByUrl, sendLocation, sendContact, sendPoll, forwardMessages, sendStory, sendEmail, replyEmail, forwardEmail.

Ключ — любая строка, уникальная для сообщения в вашем приложении: id записи в вашей базе, uuid4, order-1532-reminder.

{"chatId": "8695836", "message": "Напоминаем о записи завтра в 10:00", "clientMessageId": "booking-1532-reminder"}

Правила

Ситуация Что ответит Gratio
Повтор с тем же ключом и тем же содержимым тот же idMessage (и urlFile), второго сообщения нет
Тот же ключ, другое содержимое 409 idempotency_conflict — новый текст не уходит
Без ключа каждый вызов — новое сообщение

Gratio хранит итог по ключу и при повторе к мессенджеру не обращается. Если итог ещё не известен, Gratio повторяет отправку так, что мессенджер узнаёт её и не создаёт второе сообщение.

forwardMessages пересылает каждое сообщение отдельной отправкой с ключом clientMessageId:<номер по порядку>: повтор запроса возвращает те же id и ничего не пересылает заново.

Обрыв посреди отправки

Если связь с мессенджером оборвалась, когда сообщение уже уходило, Gratio до 15 секунд ждёт связь и повторяет отправку с тем же ключом. Получатель увидит одно сообщение, приложение — обычный ответ 200.

Не вышло за 15 секунд — ответ 503 outcome_unknown: дошло ли сообщение, неизвестно.

  1. Повторяйте с тем же clientMessageId — второго сообщения не будет.
  2. Не отправляйте то же сообщение в другой канал, пока нет итога.
  3. Итог Gratio найдёт сам по истории чата:
    • нашёл — придут уведомления outgoingAPIMessageReceived и outgoingMessageStatus со статусом sent;
    • не нашёл за 10 минут — outgoingMessageStatus со статусом failed, пустым idMessage и вашим clientMessageId.
{"typeWebhook": "outgoingMessageStatus", "chatId": "8695836", "idMessage": "",
 "clientMessageId": "booking-1532-reminder", "status": "failed", "sendByApi": true, "timestamp": 1791823700}

Сбой после отправки

Если сообщение ушло, а у Gratio что-то сломалось уже после, это не превращается в ошибку: ответ — 200 с idMessage, уведомления допишутся, когда сбой пройдёт.

Почта

У писем ключ превращается в Message-ID письма: повтор с тем же ключом не создаёт второе письмо, даже если первое уже лежит в «Отправленных».

Рецепт

import time, uuid
import httpx

def send(api, token, chat_id, text, key=None):
    key = key or str(uuid.uuid4())
    for attempt in range(5):
        try:
            r = api.post(f"/sendMessage/{token}", json={
                "chatId": chat_id, "message": text, "clientMessageId": key,
            }, timeout=90)
        except httpx.TransportError:
            continue                      # сеть — повторяем с тем же ключом
        if r.status_code == 200:
            return r.json()["idMessage"]
        if r.status_code in (429, 469, 503) or r.status_code >= 500:
            delay = int(r.headers.get("Retry-After", "5"))
            time.sleep(min(delay, 60))
            continue                      # повтор с тем же ключом безопасен
        r.raise_for_status()              # 4xx — исправить запрос, не повторять
    raise RuntimeError(f"не отправлено, ключ {key}: ждите outgoingMessageStatus")

Сохраните ключ рядом с записью в своей базе: по нему вы свяжете уведомление failed с исходным сообщением.

Что дальше

Была ли статья полезна?