Postmark - транзакционный email-сервис с фокусом на скорость и надёжность доставки: среднее время доставки 2-3 секунды, delivery rate 98%+. В отличие от Mailchimp или MailerLite (bulk email), Postmark специализируется на одиночных транзакционных письмах: подтверждения, уведомления, счета, ссылки на документы. Для Kommo интеграция решает конкретные задачи: автоматически отправить письмо при смене этапа воронки и получать входящие ответы обратно в Kommo.
Postmark API использует заголовок X-Postmark-Server-Token: {SERVER_TOKEN}. Отправка письма: POST /email. Шаблоны: POST /email/withTemplate. Inbound (входящий email): Postmark парсит входящие письма и отправляет webhook на ваш endpoint.
Postmark Server - изолированная среда для одного типа писем. Транзакционные письма и bulk-рассылки должны быть в разных серверах - это фундаментальное правило Postmark для защиты репутации домена.
Архитектура использования с Kommo
Kommo: сделка -> этап "Договор подписан"
-> Kommo webhook -> Ваш сервер
Ваш сервер
-> Kommo API: получить email, имя, детали сделки
-> Postmark: POST /email/withTemplate
{to, template_alias: "deal-won-confirmation",
template_model: {name, deal_id, amount}}
-> Kommo: note "Email-подтверждение отправлен"
Клиент отвечает на письмо
-> Postmark Inbound webhook -> Ваш сервер
-> Kommo: note с текстом ответа
Реализация: исходящий email
import requests, os
from flask import Flask, request, jsonify
app = Flask(__name__)
POSTMARK_TOKEN = os.environ["POSTMARK_SERVER_TOKEN"]
POSTMARK_BASE = "https://api.postmarkapp.com"
POSTMARK_HDR = {
"X-Postmark-Server-Token": POSTMARK_TOKEN,
"Content-Type": "application/json",
"Accept": "application/json",
}
FROM_EMAIL = os.environ["POSTMARK_FROM_EMAIL"] # verified sender
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
STAGE_EMAIL_MAP = {
int(os.environ.get("STAGE_PROPOSAL", "0")): ("proposal-sent", "Коммерческое предложение отправлено"),
int(os.environ.get("STAGE_SIGNED", "0")): ("deal-won-confirmation", "Подтверждение сделки отправлено"),
int(os.environ.get("STAGE_INVOICE", "0")): ("invoice-notification", "Счёт отправлен на email"),
}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
def get_lead_email_details(lead_id: int) -> tuple[str, str, dict]:
r = requests.get(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
params={"with": "contacts,custom_fields_values"},
)
lead = r.json()
contacts = lead.get("_embedded", {}).get("contacts", [])
email = name = ""
if contacts:
rc = requests.get(
f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
headers=KOMMO_HDR,
params={"with": "custom_fields_values"},
)
c = rc.json()
name = c.get("name", "")
for cf in c.get("custom_fields_values", []) or []:
if cf.get("field_code") == "EMAIL":
vals = cf.get("values", [])
if vals:
email = vals[0].get("value", "")
break
model = {
"client_name": name,
"deal_id": str(lead_id),
"deal_name": lead.get("name", ""),
"amount": str(lead.get("price") or 0),
}
return email, name, model
def send_template_email(to: str, template_alias: str, model: dict, tag: str) -> str:
r = requests.post(
f"{POSTMARK_BASE}/email/withTemplate",
headers=POSTMARK_HDR,
json={
"From": FROM_EMAIL,
"To": to,
"TemplateAlias": template_alias,
"TemplateModel": model,
"MessageStream": "outbound",
"Tag": tag,
"ReplyTo": FROM_EMAIL,
"Metadata": {
"kommo_lead_id": str(model.get("deal_id", "")),
},
},
)
r.raise_for_status()
return r.json().get("MessageID", "")
def add_note(lead_id: int, text: str):
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": lead_id,
"entity_type": "leads",
"note_type": "common",
"params": {"text": text},
}],
)
@app.route("/webhooks/kommo", methods=["POST"])
def kommo_webhook():
data = request.json or {}
for lead_data in data.get("leads", {}).get("status", []):
lead_id = lead_data.get("id")
new_status = lead_data.get("status_id")
stage_info = STAGE_EMAIL_MAP.get(new_status)
if not stage_info:
continue
template_alias, note_text = stage_info
email, name, model = get_lead_email_details(lead_id)
if not email:
continue
msg_id = send_template_email(email, template_alias, model, tag=template_alias)
add_note(lead_id, f"Postmark: {note_text}. MessageID: {msg_id}")
return jsonify({"status": "ok"}), 200
Реализация: Postmark Inbound (входящие ответы)
@app.route("/webhooks/postmark/inbound", methods=["POST"])
def postmark_inbound():
event = request.json or {}
from_email = event.get("FromFull", {}).get("Email", "")
subject = event.get("Subject", "")
text_body = event.get("TextBody", "") or event.get("StrippedTextReply", "")
html_body = event.get("HtmlBody", "")
# Извлечь kommo_lead_id из headers или subject
headers = {h.get("Name", ""): h.get("Value", "") for h in event.get("Headers", [])}
lead_id = headers.get("X-Kommo-Lead-Id", "")
# Fallback: найти по email отправителя
if not lead_id:
lead_id = find_lead_by_email(from_email)
lead_id = str(lead_id) if lead_id else ""
if not lead_id:
return jsonify({"status": "no_lead_id"}), 200
body_preview = (text_body or html_body)[:500].strip()
add_note(
int(lead_id),
f"Входящий email от {from_email}.
Тема: {subject}
{body_preview}",
)
return jsonify({"status": "ok"}), 200
def find_lead_by_email(email: str) -> int | None:
r = requests.get(
f"{KOMMO_BASE}/contacts",
headers=KOMMO_HDR,
params={"query": email, "limit": 5},
)
contacts = r.json().get("_embedded", {}).get("contacts", []) or []
if not contacts:
return None
r2 = requests.get(
f"{KOMMO_BASE}/leads",
headers=KOMMO_HDR,
params={"filter[contact_id]": contacts[0]["id"], "limit": 1},
)
leads = r2.json().get("_embedded", {}).get("leads", []) or []
return leads[0]["id"] if leads else None
Postmark Templates
Создайте шаблоны в Postmark Dashboard -> Templates. Пример шаблона deal-won-confirmation:
Subject: Ваша сделка подтверждена, {{client_name}}!
Уважаемый {{client_name}},
Рады сообщить, что сделка {{deal_name}} (ID: {{deal_id}}) оформлена.
Сумма: ${{amount}}.
С уважением,
Команда [Компания]
Template variables {{variable}} заполняются из TemplateModel в API-запросе.
Postmark Inbound: настройка
Postmark Dashboard -> Servers -> [Server] -> Settings -> Inbound:
- Inbound domain:
inbound.yourdomain.com(или используйте Postmark-provided) - Webhook URL:
https://your-server.com/webhooks/postmark/inbound
MX-запись: inbound.yourdomain.com -> mx.postmarkapp.com (или настроить email forwarding если нет своего домена).
Для передачи lead_id в ответ клиента: добавьте кастомный заголовок X-Kommo-Lead-Id при отправке:
# В send_template_email - добавить Headers
json={
...
"Headers": [
{"Name": "X-Kommo-Lead-Id", "Value": str(model.get("deal_id", ""))},
],
}
При ответе клиент сохраняет заголовок (большинство email-клиентов) - Postmark Inbound его распарсит.
Postmark vs SendGrid vs AWS SES
| Postmark | SendGrid | AWS SES | |
|---|---|---|---|
| Delivery (транз.) | 98%+ | 96% | 95% |
| Скорость | 2-3 сек | 5-10 сек | 3-5 сек |
| Цена (10k email) | $15 | $19.95 | $1 |
| Inbound parsing | Да | Да (дорого) | Нет |
| Шаблоны | Да | Да | Нет |
Postmark дороже SES, но delivery rate и скорость - лучшие в классе. Для транзакционных писем где важна гарантия доставки - Postmark оправдан.
Для кого актуально
B2B SaaS и сервисные компании, где каждое транзакционное письмо критично: подтверждение сделки, счёт, ссылка на договор. Особенно для компаний, уже столкнувшихся с попаданием в спам через общий email (Gmail SMTP, G Suite SMTP) - Postmark решает проблему репутации домена.
Для bulk email-маркетинга смотрите Kommo + MailerLite.
Часто задаваемые вопросы
Нужно ли верифицировать домен в Postmark?
Обязательно. Без верификации домена (DKIM + SPF) письма будут идти с пометкой “via postmarkapp.com” и иметь низкий deliverability. Верификация: Postmark Dashboard -> Sender Signatures -> Add Domain -> добавить DNS-записи. Занимает 5-10 минут.
Как Postmark обрабатывает bounces?
Postmark автоматически обрабатывает hard и soft bounces. При hard bounce (несуществующий email) Postmark деактивирует адрес и больше не отправляет на него. Bounce webhook - POST /webhooks event типа bounce - можно использовать для обновления контакта в Kommo (добавить тег “email_invalid”).
Как работает Message Streams в Postmark?
Message Streams разделяют транзакционные и broadcast (маркетинговые) письма в рамках одного сервера. MessageStream: "outbound" - транзакционные. "broadcast" - маркетинговые. Разные потоки = разные IP = разная репутация. Это критично: одна неудачная broadcast-кампания не влияет на доставку транзакционных писем.
Итог
Kommo + Postmark - транзакционный email из воронки:
X-Postmark-Server-Tokenзаголовок,POST /email/withTemplateс TemplateAlias- Metadata
kommo_lead_idдля обратной корреляции - Inbound webhook: входящие ответы клиентов -> note в Kommo
X-Kommo-Lead-Idheader в исходящих письмах для надёжной корреляции ответов- Message Streams: транзакционные и маркетинговые письма - разные потоки
Если нужна интеграция Postmark с Kommo или настройка транзакционных писем из воронки - опишите задачу команде Exceltic.dev.