Twilio SendGrid is the standard for transactional email in B2B SaaS: Dynamic Templates with Handlebars, Event Webhook for tracking, 99.9% delivery rate SLA. There is no native integration with Kommo. We build via SendGrid Mail Send API v3.
What we’re building
- Won deal -> send onboarding email via SendGrid Dynamic Template
- SendGrid Event Webhook -> open/click/bounce -> Note in Kommo
- Full scenario: email sequence by pipeline stage without an external ESP automation tool
Authentication
SendGrid uses a Bearer token (API Key):
import requests
SENDGRID_API_KEY = "SG.your_api_key"
sg_session = requests.Session()
sg_session.headers.update({
"Authorization": f"Bearer {SENDGRID_API_KEY}",
"Content-Type": "application/json",
})
SEND_URL = "https://api.sendgrid.com/v3/mail/send"
API Key is created in SendGrid Dashboard -> Settings -> API Keys. Minimum permissions for sending: Mail Send (Full Access).
Sending via Dynamic Template
def send_onboarding_email(to_email: str, to_name: str, template_data: dict, deal_id: int) -> str:
"""Send email via SendGrid Dynamic Template. Returns message ID."""
payload = {
"from": {
"email": "hello@exceltic.dev",
"name": "Exceltic",
},
"personalizations": [
{
"to": [{"email": to_email, "name": to_name}],
"dynamic_template_data": {
**template_data,
"kommo_deal_id": str(deal_id), # for tracking in webhook
},
"custom_args": {
"kommo_deal_id": str(deal_id), # passed in every Event
},
}
],
"template_id": "d-your_template_id", # Template ID from SendGrid
"tracking_settings": {
"click_tracking": {"enable": True},
"open_tracking": {"enable": True},
},
"mail_settings": {
"bypass_list_management": {"enable": False},
},
}
r = sg_session.post(SEND_URL, json=payload)
r.raise_for_status()
# SendGrid returns 202 with no body. Message ID is in the X-Message-Id header
return r.headers.get("X-Message-Id", "")
custom_args is the key tool: these fields are returned in every Event from the Event Webhook. This tells you which Kommo deal a given event belongs to, without needing an external database.
Event Webhook: open tracking
SendGrid sends an array of events to your URL on each delivered, opened, clicked, bounced, unsubscribed.
ECDSA signature verification:
SendGrid signs webhooks via ECDSA (not HMAC). This is non-standard - most webhooks use HMAC-SHA256.
from ecdsa import VerifyingKey, NIST256p
from ecdsa.util import sigdecode_der
import base64, hashlib
from flask import Flask, request, abort
app = Flask(__name__)
# Public key from SendGrid Dashboard -> Settings -> Mail Settings -> Event Webhook
SG_VERIFICATION_KEY = "MFkwEwYHKoZIzj0CAQY..." # base64 DER-encoded public key
def verify_sendgrid_signature(payload: bytes, timestamp: str, signature: str) -> bool:
"""Verify SendGrid Event Webhook ECDSA signature."""
vk = VerifyingKey.from_der(base64.b64decode(SG_VERIFICATION_KEY))
signed_payload = timestamp.encode() + payload
digest = hashlib.sha256(signed_payload).digest()
try:
return vk.verify_digest(
base64.b64decode(signature),
digest,
sigdecode=sigdecode_der,
)
except Exception:
return False
@app.route("/sendgrid/events", methods=["POST"])
def sendgrid_events():
timestamp = request.headers.get("X-Twilio-Email-Event-Webhook-Timestamp", "")
signature = request.headers.get("X-Twilio-Email-Event-Webhook-Signature", "")
if not verify_sendgrid_signature(request.get_data(), timestamp, signature):
abort(401)
events = request.json # array of events
for event in events:
process_event(event)
return "ok", 200
def process_event(event: dict):
event_type = event.get("event", "") # "delivered", "open", "click", "bounce"
deal_id_str = event.get("kommo_deal_id", "") # from custom_args
email = event.get("email", "")
url = event.get("url", "") # only for "click"
reason = event.get("reason", "") # only for "bounce"
if not deal_id_str:
return
deal_id = int(deal_id_str)
if event_type == "open":
add_kommo_note(deal_id, f"Email opened: {email}")
elif event_type == "click":
add_kommo_note(deal_id, f"Link clicked: {url}")
elif event_type in ("bounce", "dropped", "blocked"):
add_kommo_note(deal_id, f"Email not delivered ({event_type}): {reason}")
create_kommo_task(deal_id, f"Check client email: {email}")
ecdsa library: pip install ecdsa. Without verification - the webhook is accessible to anyone who knows the URL.
Email sequence by pipeline stage
Instead of an external ESP automation tool (additional cost) - logic handled on your service side:
STAGE_TEMPLATES = {
PROPOSAL_STAGE_ID: "d-template_proposal",
NEGOTIATION_STAGE_ID:"d-template_followup",
WON_STAGE_ID: "d-template_onboarding",
}
@app.route("/kommo/stage-webhook", methods=["POST"])
def kommo_stage_webhook():
data = request.json
for lead in data.get("leads", {}).get("update", []):
new_status = lead.get("status_id")
deal_id = lead["id"]
if new_status in STAGE_TEMPLATES:
contact = get_kommo_deal_contact(deal_id)
send_onboarding_email(
to_email=contact["email"],
to_name=contact["name"],
template_data={"deal_name": lead.get("name", "")},
deal_id=deal_id,
)
return "ok", 200
A single integration point - all email triggers are managed via the STAGE_TEMPLATES config.
Real case
A EU SaaS company with 60-70 deals per month was sending onboarding emails manually through the SendGrid UI. 12-15% of deals were missing emails due to human error (forgot to send, wrong template).
After the integration:
- Onboarding email is sent automatically when the deal moves to Won (delay < 5 seconds)
- Bounce events create tasks for the manager immediately
- Open tracking updates a field in Kommo - the manager sees whether the client has read the email
Over the first 3 months: 0 missed onboarding emails, 23% increase in response rate.
Who this is for
SaaS and B2B teams that already use SendGrid for transactional email and want to close the feedback loop in the CRM. Particularly relevant when event tracking (opens, clicks) is needed in the context of a specific deal.
For full email automation with a visual editor - see Kommo + Customer.io or Kommo + Loops.
Frequently asked questions
How do Dynamic Templates differ from Legacy Templates?
Dynamic Templates use Handlebars {{variable}} syntax and are managed via the Design Editor in SendGrid. Legacy Templates are deprecated and use {{%variable%}}. For all new integrations - Dynamic Templates only. Template IDs start with d-.
How to configure a separate webhook for tracking vs transactional events?
SendGrid allows one Event Webhook URL per account. Separation can be done via custom_args: add "event_category": "tracking" or "transactional" and filter in the handler. Alternatively, use subusers - each has its own Event Webhook.
Is IP Warmup needed for sending?
If sending from a dedicated IP - yes, warmup is required. If via shared IP pool (default) - no. For volumes up to 100K emails per month, a shared IP is sufficient. A dedicated IP is recommended from 200K+ or when full reputation isolation is needed.
How to handle unsubscribes in Kommo?
The unsubscribe event from SendGrid contains the email. The handler finds the contact in Kommo by email and sets the tag email_unsubscribed or updates a custom field. On the next send, the service checks this tag and skips the contact.
Summary
The Kommo + SendGrid integration delivers a closed-loop email communication cycle in the CRM:
- Bearer token for sending,
custom_argsfor passing deal_id into events - Event Webhook: ECDSA verification via
X-Twilio-Email-Event-Webhook-Signature - Open/click/bounce -> Note in Kommo without manual work
- Email sequence by pipeline stage without an external automation tool
If SendGrid is already in your stack and you need to feed events back into Kommo - describe the task to the Exceltic.dev team.