Kommo + Omnisend: email and SMS automation for e-commerce from the pipeline

Omnisend is an omnichannel e-commerce platform: email, SMS, push notifications, and popups in one tool with native integration for Shopify, WooCommerce, and BigCommerce. Unlike Drip or Klaviyo, Omnisend is stronger in SMS + email in a single workflow and e-commerce event automation. Without a Kommo integration, the marketer does not know which Omnisend subscribers have already become paying clients and on which plan. With the integration, Won -> Omnisend profile is instantly updated -> a post-purchase sequence for the new client is triggered.

Omnisend vs Klaviyo vs Drip for e-commerce

ParameterOmnisendKlaviyoDrip
SMSYes, nativeYes (SMS add-on)Yes (US only)
Push notificationsYesNoNo
E-commerce automationShopify/WooCommerce nativeShopify nativeAPI-based
PriceFrom $16/monthFrom $20/monthFrom $39/month
APIREST with API KeyREST with API KeyREST Basic Auth

Omnisend is chosen by teams with Shopify + a dedicated sales team: native store integration + CRM context via Kommo.

What is synchronized

Kommo -> Omnisend:
— Won -> create/update Contact with tags customer, plan_{tier}, channel_{source}
— Won -> set custom fields: plan, mrr, deal_id, won_date
— Won -> add to “Paying customers” segment (via tag)
— Plan change -> update tag and custom field plan
— Customer lost -> add tag churned, remove active

Omnisend -> Kommo:
contact.unsubscribed -> Note + task for manager
contact.bounced -> Note: verify contact email
automation.email.clicked -> Note (optional for key emails)

Omnisend API: key requests

Base URL: https://api.omnisend.com/v3.
Authentication: X-API-KEY: {api_key} in the header.
API Key: Omnisend -> Store Settings -> Integrations -> API Keys.

import requests
from datetime import datetime, timezone

OMNISEND_API_KEY = "your_api_key"
OMNISEND_BASE_URL = "https://api.omnisend.com/v3"
HEADERS = {
    "X-API-KEY": OMNISEND_API_KEY,
    "Content-Type": "application/json",
}

def upsert_contact(email: str, tags: list, custom_props: dict,
                   first_name: str = "", phone: str = "") -> dict:
    # Create or update a contact. Omnisend upserts by email
    payload = {
        "email": email,
        "status": "subscribed",
        "statusDate": datetime.now(timezone.utc).isoformat(),
        "tags": tags,
    }
    if first_name:
        payload["firstName"] = first_name
    if phone:
        payload["phone"] = phone  # E.164 format: +31612345678
    if custom_props:
        payload["customProperties"] = custom_props

    resp = requests.post(
        f"{OMNISEND_BASE_URL}/contacts",
        headers=HEADERS,
        json=payload
    )
    # 200 = updated, 201 = created, both are OK
    if resp.status_code not in (200, 201):
        resp.raise_for_status()
    return resp.json()

def add_tags(contact_id: str, tags: list) -> None:
    resp = requests.post(
        f"{OMNISEND_BASE_URL}/contacts/{contact_id}/tags",
        headers=HEADERS,
        json={"tags": tags}
    )
    resp.raise_for_status()

def remove_tag(contact_id: str, tag: str) -> None:
    resp = requests.delete(
        f"{OMNISEND_BASE_URL}/contacts/{contact_id}/tags/{tag}",
        headers=HEADERS
    )
    if resp.status_code != 404:
        resp.raise_for_status()

def get_contact_by_email(email: str) -> dict | None:
    resp = requests.get(
        f"{OMNISEND_BASE_URL}/contacts",
        headers=HEADERS,
        params={"email": email}
    )
    resp.raise_for_status()
    contacts = resp.json().get("contacts", [])
    return contacts[0] if contacts else None

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    name = contact["name"]
    first_name = name.split()[0] if name else ""
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "starter"
    source = get_custom_field(lead, SOURCE_FIELD_ID) or "direct"
    amount = lead.get("price", 0)
    phone = get_contact_phone(contact) or ""

    tags = ["customer", f"plan_{plan}", f"source_{source}", "active"]

    custom_props = {
        "plan": plan,
        "mrr": str(amount),
        "kommo_deal_id": str(lead["id"]),
        "won_date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
    }

    omnisend_contact = upsert_contact(
        email=email,
        tags=tags,
        custom_props=custom_props,
        first_name=first_name,
        phone=phone,
    )

    create_kommo_note(lead["id"],
        f"Omnisend: contact updated (tags: customer, plan_{plan})")

def on_plan_upgrade(lead: dict, contact: dict, old_plan: str, new_plan: str):
    email = get_contact_email(contact)
    c = get_contact_by_email(email)
    if not c:
        return
    contact_id = c["contactID"]
    remove_tag(contact_id, f"plan_{old_plan}")
    add_tags(contact_id, [f"plan_{new_plan}", "upgraded"])

def on_deal_lost(lead: dict, contact: dict):
    email = get_contact_email(contact)
    c = get_contact_by_email(email)
    if not c:
        return
    contact_id = c["contactID"]
    remove_tag(contact_id, "active")
    add_tags(contact_id, ["churned"])

Handle Omnisend Webhook:

from flask import Flask, request

app = Flask(__name__)

@app.route("/webhooks/omnisend", methods=["POST"])
def omnisend_webhook():
    payload = request.json
    event = payload.get("event")
    contact = payload.get("contact", {})
    email = contact.get("email", "")

    if not email:
        return "", 200

    deal_id = find_kommo_deal_by_contact_email(email)
    if not deal_id:
        return "", 200

    if event == "contact.unsubscribed":
        create_kommo_note(deal_id,
            "Omnisend: client unsubscribed from mailings")
        create_kommo_task(deal_id,
            "Clarify communication preferences - client unsubscribed from Omnisend")

    elif event == "contact.bounced":
        create_kommo_note(deal_id,
            "Omnisend: email not delivered (bounce)")
        create_kommo_task(deal_id,
            "Verify email - Omnisend recorded a hard bounce")

    return "", 200

Setting up a Webhook in Omnisend: Store Settings -> Integrations -> Webhooks -> Add Webhook. Select events and specify the URL.

SMS automation: Omnisend’s unique advantage

For EU e-commerce teams, the SMS channel in Omnisend is particularly valuable: EU rates are lower than US, and GDPR opt-in/opt-out support is built in. On Won with a phone number — Omnisend automatically enrolls the contact in the SMS last-step if email is not opened.

# On Won with phone number - set SMS status
if phone:
    payload["phone"] = phone
    payload["phoneStatus"] = "subscribed"  # GDPR: only if consent is given
    payload["phoneStatusDate"] = datetime.now(timezone.utc).isoformat()

Real-world case

DTC e-commerce SaaS (EU, Shopify + Kommo + Omnisend, 40–60 new clients per month):

  • Before: Omnisend segmented all subscribers without distinguishing “purchased / not purchased”. The post-purchase sequence went to everyone — including those who had not yet become clients. Onboarding emails without CRM context.
  • After: Won -> tag customer + custom fields plan/mrr -> the correct segment is triggered in Omnisend. Trial users and paying clients receive different sequences. Upgrade emails do not go to those already on the top plan.
  • Additionally: when the churned tag is applied -> Omnisend starts a win-back automation. 9% of churned clients recovered over 6 months.

Who this is relevant for

  • E-commerce with a dedicated sales team: Shopify + managers
  • Teams with an SMS channel in EU — Omnisend natively supports EU carriers
  • SaaS with a subscription model: different email sequences for each plan
  • Companies with 30+ active clients where segmentation already matters

Frequently asked questions

Omnisend vs Klaviyo — when to choose which for e-commerce?

Klaviyo: if you have Shopify and need the richest Shopify triggers (abandoned cart with specific items, Browse Abandonment, Price Drop). Omnisend: if SMS + email in one workflow are needed, more affordable pricing, slightly simpler UI. For Kommo + Klaviyo — similar architecture, the difference is in platform capabilities.

Omnisend customProperties — how do I create a custom field?

Via the API: on the first POST /contacts with customProperties: {"field_name": "value"}, Omnisend automatically creates the custom property. It is then available in segmentation and personalization. Field name — any string; best practice: snake_case.

Omnisend webhook — how do I secure the endpoint?

Omnisend does not sign webhooks with HMAC. Protection: secret URL (/webhooks/omnisend/{random_secret}) + optional IP whitelist. Omnisend publishes their IP addresses in the documentation.

Does Omnisend support GDPR double opt-in?

Yes. When creating a contact via API, you can specify status: "unconfirmed" — the contact will receive a confirmation email. After confirmation, the status changes to subscribed. For EU teams this is mandatory when there is no explicit consent at registration.

Summary

  • Omnisend API: X-API-KEY in header, https://api.omnisend.com/v3
  • Upsert contact: POST /contacts — 200/201 both OK
  • Tags: POST /contacts/{id}/tags, remove: DELETE /contacts/{id}/tags/{tag}
  • customProperties: created automatically on first API use
  • SMS: pass phone (E.164) and phoneStatus: "subscribed" only with consent
  • Webhook: no HMAC — protection via secret path + IP whitelist

If you use Omnisend and Kommo and want to sync CRM statuses with email/SMS segments — describe your plan structure and current automation workflows. Exceltic.dev will configure the mapping and event handling.

More articles

All →