Ortto (formerly Autopilot) is a Customer Data Platform (CDP) with built-in email automation, SMS, in-app messaging, and customer journey analytics. Unlike standard email tools, Ortto builds a unified user profile from all channels (web, email, CRM, product) and triggers automations based on behavior. For Kommo, the Ortto integration lets you push pipeline stage data into the CDP and launch personalized email sequences based on deal progress.
Ortto’s API uses the X-Api-Key header. Key operations: upsert person (create or update a profile), track activity (record an event), trigger journey (start an automation). Bidirectional: Ortto can send webhooks on specific events (email opened, link clicked, unsubscribed).
Ortto Journey is a visual automation builder: when an event (activity) occurs, launch a sequence of emails/SMS/tasks. The equivalent of Sequences in HubSpot or Automation in ActiveCampaign.
Why Ortto Instead of Mailchimp or ActiveCampaign
Ortto builds a unified person profile from multiple sources. If a contact entered Kommo as a lead, then became a product user, then contacted support - all of that becomes one profile in Ortto. ActiveCampaign and Mailchimp operate as isolated email tools without a CDP layer.
For B2B SaaS with a long sales cycle this means: email campaigns account not only for the stage in Kommo, but also for how the user interacts with the product (logins, features used, depth of usage).
Integration Architecture
Kommo: deal moves to a new stage
-> Kommo webhook -> Your server
-> Ortto API: upsert person (email, name, kommo_stage)
-> Ortto API: track activity "crm_stage_changed"
Ortto Journey: trigger "crm_stage_changed" where stage = "Qualification"
-> Email 1: "How did the first call go?"
-> Wait 2 days
-> Email 2: Case study relevant to the segment
-> Wait 3 days -> Check: did they open the email?
-> Branch: opened -> task for manager in Kommo
Ortto webhook: email opened, link clicked
-> Your server
-> Kommo: update "Email activity" field
Implementation: upsert person + track activity
import requests, os
from flask import Flask, request, jsonify
app = Flask(__name__)
ORTTO_API_KEY = os.environ["ORTTO_API_KEY"]
ORTTO_REGION = os.environ.get("ORTTO_REGION", "api") # api (US) or api.eu (EU)
KOMMO_SUBDOMAIN = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN = os.environ["KOMMO_ACCESS_TOKEN"]
KOMMO_STAGE_MAP = {
# stage_id -> (ortto_stage_label, journey_activity)
1234: ("Новый лид", "crm_stage_new_lead"),
1235: ("Квалификация", "crm_stage_qualification"),
1236: ("КП отправлено", "crm_stage_proposal_sent"),
1237: ("Переговоры", "crm_stage_negotiation"),
1238: ("Закрыта", "crm_stage_closed_won"),
}
ORTTO_BASE = f"https://{ORTTO_REGION}.ap3api.com"
ORTTO_HDR = {"X-Api-Key": ORTTO_API_KEY, "Content-Type": "application/json"}
KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR = {"Authorization": f"Bearer {KOMMO_TOKEN}", "Content-Type": "application/json"}
def get_lead_contact(lead_id: int) -> tuple[dict, 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", [])
contact = {}
if contacts:
rc = requests.get(
f"{KOMMO_BASE}/contacts/{contacts[0]['id']}",
headers=KOMMO_HDR,
params={"with": "custom_fields_values"},
)
contact = rc.json()
return lead, contact
def extract_email_and_name(contact: dict) -> tuple[str, str, str]:
email = ""
for cf in contact.get("custom_fields_values", []) or []:
if cf.get("field_code") == "EMAIL":
vals = cf.get("values", [])
if vals:
email = vals[0].get("value", "")
break
full_name = contact.get("name", "")
parts = full_name.split(" ", 1)
first = parts[0] if parts else ""
last = parts[1] if len(parts) > 1 else ""
return email, first, last
def ortto_upsert_person(email: str, first: str, last: str, kommo_lead_id: int, stage: str):
payload = {
"people": [{
"fields": {
"str::email": {"v": email},
"str::first": {"v": first},
"str::last": {"v": last},
"str::kommo_lead_id": {"v": str(kommo_lead_id)},
"str::kommo_stage": {"v": stage},
}
}],
"merge_by": ["str::email"],
"merge_strategy": 2, # MERGE: update the existing profile
}
r = requests.post(f"{ORTTO_BASE}/v1/persons/merge", headers=ORTTO_HDR, json=payload)
r.raise_for_status()
return r.json().get("people", [{}])[0].get("id", "")
def ortto_track_activity(person_id: str, activity_id: str, attributes: dict):
payload = {
"activities": [{
"activity_id": activity_id, # "act:cm:crm-stage-changed" (configured in Ortto)
"person_id": person_id,
"fields": {k: {"v": v} for k, v in attributes.items()},
}]
}
requests.post(f"{ORTTO_BASE}/v1/activities/create", headers=ORTTO_HDR, json=payload)
@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 = KOMMO_STAGE_MAP.get(new_status)
if not stage_info:
continue
stage_label, activity_id = stage_info
lead, contact = get_lead_contact(lead_id)
email, first, last = extract_email_and_name(contact)
if not email:
continue
person_id = ortto_upsert_person(email, first, last, lead_id, stage_label)
ortto_track_activity(person_id, activity_id, {
"str::lead_id": str(lead_id),
"str::stage": stage_label,
"dbl::deal_value": str(lead.get("price", 0) or 0),
})
return jsonify({"status": "ok"}), 200
Feedback Loop: Ortto Webhook -> Kommo
Ortto sends a webhook on: email opened, link clicked, unsubscribed, journey completed.
KOMMO_CF_EMAIL_ACTIVITY = int(os.environ["KOMMO_CF_EMAIL_ACTIVITY"])
@app.route("/webhooks/ortto", methods=["POST"])
def ortto_webhook():
event = request.json or {}
ev_type = event.get("type", "")
if ev_type not in ("email.opened", "email.link_clicked"):
return jsonify({"status": "ignored"}), 200
person = event.get("person", {})
fields = person.get("fields", {})
lead_id = fields.get("str::kommo_lead_id", {}).get("v", "")
email_subject = event.get("email", {}).get("subject", "")
if not lead_id:
return jsonify({"status": "no_lead_id"}), 200
action_label = "opened email" if ev_type == "email.opened" else "clicked in email"
note_text = f"Ortto: contact {action_label}: '{email_subject}'"
requests.post(
f"{KOMMO_BASE}/notes",
headers=KOMMO_HDR,
json=[{
"entity_id": int(lead_id),
"entity_type": "leads",
"note_type": "common",
"params": {"text": note_text},
}],
)
# Update custom field "Last email activity"
requests.patch(
f"{KOMMO_BASE}/leads/{lead_id}",
headers=KOMMO_HDR,
json={"custom_fields_values": [{
"field_id": KOMMO_CF_EMAIL_ACTIVITY,
"values": [{"value": f"{ev_type}: {email_subject}"}],
}]},
)
return jsonify({"status": "ok"}), 200
Setting Up Custom Fields in Ortto
To store data from Kommo you need to add custom fields in Ortto:
-
Ortto -> Settings -> Data -> Person Fields -> Add Field
kommo_lead_id(Text): deal ID in Kommokommo_stage(Text): current pipeline stage
-
Ortto -> Activities -> Add Activity
crm_stage_changed: stage change event- Activity fields:
lead_id(Text),stage(Text),deal_value(Number)
-
Ortto -> Journeys -> New Journey
- Trigger: Activity
crm_stage_changedwhere stage = “Qualification” - Step 1: Send email “First call results”
- Wait: 2 days, unless email opened (Early exit)
- Step 2: Send case study email
- Trigger: Activity
Real-World Case
B2B SaaS, 150 leads per month. Before Ortto: mass email blasts with no personalization by pipeline stage. Email-to-meeting conversion rate: 2.1%. After Kommo + Ortto integration: email sequences launch automatically on stage change, subject lines personalized by segment. Conversion rate: 4.8%. Additional meetings: +23 per month.
Who This Is For
B2B SaaS with a long sales cycle (30+ days) and a team of 10-50 people. Especially if marketing and sales use different tools and there is a gap between them - Ortto as a CDP bridges it. Ortto is more expensive than Mailchimp ($99+/month), but cheaper than Marketo or Eloqua.
A similar approach to email automation is described for Kommo + Customer.io and Kommo + Campaign Monitor.
Frequently Asked Questions
How does merge_strategy work in the Ortto person upsert?
merge_strategy: 1 (OVERWRITE) - overwrites all fields of the existing profile. merge_strategy: 2 (MERGE) - updates only the fields provided, preserving the rest. For CRM integrations always use MERGE: there is no need to pass all fields from Kommo on every update.
Does Ortto store GDPR data in the EU?
Yes, when using the EU region (api.eu.ap3api.com). Select the EU Data Center when initializing your account. Accounts previously created in the US region do not have their data moved. If GDPR compliance is critical - make sure your Ortto account was created in the EU.
How do I sync unsubscribes from Ortto back to Kommo?
The person.unsubscribed webhook contains person.fields with kommo_lead_id. Update the “Email status” custom field in Kommo with the value “Unsubscribed”. This prevents a manager from manually sending an email through Kommo to someone who has unsubscribed from Ortto campaigns.
Summary
Kommo + Ortto - a CDP layer for B2B email automation:
X-Api-Keyheader, EU region for GDPRPOST /v1/persons/mergewithmerge_strategy: 2on Kommo stage changePOST /v1/activities/createwithactivity_id- Journey trigger- Custom field
kommo_lead_idfor reverse correlation - Ortto webhook
email.opened-> note in Kommo + update custom field
If you need a CDP integration between Kommo and Ortto - describe your pipeline to the Exceltic.dev team.