Сеть рвётся, тайм-ауты случаются. Чтобы повтор запроса не превратился во второе сообщение клиенту, передавайте в отправке свой ключ — 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: дошло ли сообщение, неизвестно.
- Повторяйте с тем же
clientMessageId— второго сообщения не будет. - Не отправляйте то же сообщение в другой канал, пока нет итога.
- Итог 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 с исходным сообщением.
Что дальше
- Файлы и ссылки на них — как отправлять и получать файлы.
