Kommo + Paddle: Automatic Subscription Creation via Merchant of Record

Kommo + Paddle: Automatic Subscription Creation via Merchant of Record

Paddle is not just a payment gateway — it is a Merchant of Record: it handles the calculation and remittance of EU VAT, UK VAT, US sales tax, and GST worldwide. Without Kommo integration, a manager closes a deal and then manually opens Paddle to create a customer and subscription — with a delay and the risk of data errors. With the integration, Won in Kommo automatically launches a Paddle subscription, and payment and cancellation events immediately appear in the deal card.

Why Paddle Differs from Stripe and Chargebee

Merchant of Record (MoR) is a model in which the payment provider acts as the legal seller in a transaction. Paddle invoices the client in its own name, collects the tax, and remits it to tax authorities.

For SaaS companies selling in the EU, UK, or US, this eliminates three problems: — EU VAT: Paddle registers for VAT in each EU country — you do not need to register in 27 countries — US sales tax: Paddle tracks nexus and remits tax by state — Compliance: invoices with Paddle’s correct VAT number, not your company’s

Compared to subscription via Chargebee, where tax liability remains yours, Paddle removes this entirely.

This is critical for a product with clients in multiple jurisdictions — and precisely why B2B SaaS companies choose Paddle when entering international markets.

What Synchronises Between Kommo and Paddle

Kommo -> Paddle: — Won -> create Paddle customer (email, name, country from deal fields) — Won -> create Paddle subscription (price_id from the “Plan” custom field) — Contact update (email/name) -> update Paddle customer

Paddle -> Kommo:subscription.created -> Note: “Paddle: subscription activated, ID = sub_xxx” — transaction.completed -> Note: “Paddle: payment $X for period” — transaction.payment_failed -> Note + manager task: “Paddle: payment failed, verify card details” — subscription.canceled -> Note + custom field paddle_status = canceledsubscription.paused -> Note + field paddle_status = paused

Integration Architecture

Kommo Webhook: deal moved to Won
  ↓ Backend
  1. GET /api/v4/leads/{id} + contacts
     -> email, name, country, plan from custom field
  2. Paddle API: POST /customers
     -> create or update customer
  3. Paddle API: POST /subscriptions
     -> price_id = plan mapping, customer_id from step 2
     -> collection_mode = "automatic"
  4. Kommo: POST /leads/{id}/notes
     -> "Paddle: subscription {subscription_id} activated"

Paddle Webhook: transaction.payment_failed
  ↓ Backend
  1. HMAC-SHA256 verification (Paddle-Signature header)
  2. Extract customer.id -> find deal by paddle_customer_id field
  3. Kommo: POST /leads/{deal_id}/notes -> "Paddle: payment failed"
  4. Kommo: POST /tasks -> "Verify client payment details in Paddle"

Paddle API: Key Requests

Base URL: https://api.paddle.com. Sandbox: https://sandbox-api.paddle.com. Authentication: Bearer token (Authorization: Bearer {api_key}).

Create a customer in Paddle:

import requests

PADDLE_API_KEY = "your_paddle_api_key"
PADDLE_BASE_URL = "https://api.paddle.com"

headers = {
    "Authorization": f"Bearer {PADDLE_API_KEY}",
    "Content-Type": "application/json"
}

def create_paddle_customer(email: str, name: str, country_code: str) -> dict:
    resp = requests.post(
        f"{PADDLE_BASE_URL}/customers",
        headers=headers,
        json={
            "email": email,
            "name": name,
            "locale": "en",
            "custom_data": {"country": country_code}
        }
    )
    resp.raise_for_status()
    return resp.json()["data"]

Create a subscription:

PLAN_TO_PRICE_ID = {
    "starter": "pri_01abc123",
    "growth":  "pri_01def456",
    "scale":   "pri_01ghi789",
}

def create_paddle_subscription(customer_id: str, plan: str) -> dict:
    price_id = PLAN_TO_PRICE_ID.get(plan)
    if not price_id:
        raise ValueError(f"Unknown plan: {plan}")

    resp = requests.post(
        f"{PADDLE_BASE_URL}/subscriptions",
        headers=headers,
        json={
            "customer_id": customer_id,
            "items": [{"price_id": price_id, "quantity": 1}],
            "collection_mode": "automatic",
        }
    )
    resp.raise_for_status()
    return resp.json()["data"]

def on_deal_won(lead: dict, contact: dict):
    email = get_contact_email(contact)
    name = contact["name"]
    plan = get_custom_field(lead, PLAN_FIELD_ID) or "starter"
    country = get_custom_field(lead, COUNTRY_FIELD_ID) or "US"

    customer = create_paddle_customer(email, name, country)
    subscription = create_paddle_subscription(customer["id"], plan)

    # Save paddle_customer_id in Kommo for reverse lookup
    update_kommo_deal(lead["id"], {
        "paddle_customer_id": customer["id"],
        "paddle_subscription_id": subscription["id"],
        "paddle_status": "active"
    })
    create_kommo_note(
        lead["id"],
        f"Paddle: subscription {subscription['id']} activated (plan {plan})"
    )

Handling Paddle Webhook with HMAC verification:

import hashlib
import hmac
from flask import Flask, request, abort

app = Flask(__name__)
PADDLE_WEBHOOK_SECRET = "your_webhook_secret"

def verify_paddle_signature(payload: bytes, signature_header: str) -> bool:
    # Paddle-Signature: ts=1234567890;h1=abc123...
    parts = dict(part.split("=", 1) for part in signature_header.split(";"))
    ts = parts.get("ts", "")
    h1 = parts.get("h1", "")
    signed_payload = f"{ts}:{payload.decode()}"
    expected = hmac.new(
        PADDLE_WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, h1)

@app.route("/webhooks/paddle", methods=["POST"])
def paddle_webhook():
    signature = request.headers.get("Paddle-Signature", "")
    if not verify_paddle_signature(request.data, signature):
        abort(403)

    payload = request.json
    event_type = payload.get("event_type")
    event_id = payload.get("event_id")  # idempotency key
    data = payload.get("data", {})

    if event_type == "transaction.payment_failed":
        customer_id = data.get("customer_id")
        amount = data.get("details", {}).get("totals", {}).get("total", "?")
        currency = data.get("currency_code", "USD")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            create_kommo_note(deal_id,
                f"Paddle: payment failed (amount {amount} {currency})")
            create_kommo_task(deal_id,
                "Verify payment details - Paddle recorded a failed payment")

    elif event_type == "subscription.canceled":
        customer_id = data.get("customer_id")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            update_kommo_deal(deal_id, {"paddle_status": "canceled"})
            create_kommo_note(deal_id,
                "Paddle: subscription cancelled")

    elif event_type == "transaction.completed":
        customer_id = data.get("customer_id")
        totals = data.get("details", {}).get("totals", {})
        amount = totals.get("total", "?")
        currency = data.get("currency_code", "USD")
        deal_id = find_deal_by_paddle_customer(customer_id)
        if deal_id:
            create_kommo_note(deal_id,
                f"Paddle: payment {amount} {currency} received")

    return "", 200

Setting Up Webhooks in Paddle: Paddle Dashboard -> Notifications -> New destination. Specify the URL and select events. The secret key is generated automatically — copy it to PADDLE_WEBHOOK_SECRET.

Paddle’s Tax Logic: What This Means in Practice

Paddle automatically determines the tax rate based on the client’s IP and billing country. For business clients from the EU — it requests a VAT number and applies reverse charge. For individuals in the EU — it applies VAT at the client’s country rate.

The invoice lists Paddle as the seller. The client receives a correct tax document without your involvement. For Kommo this means: the “plan” field in the deal -> subscription created -> Paddle handles the taxes.

Real-World Case

B2B SaaS (Europe, 40–60 new clients per month, Kommo + Paddle; Stripe was replaced by Paddle as MoR):

  • Before: at each Won the manager manually opened Paddle, created the customer, and selected the plan. Delay of 30–90 minutes, and sometimes the wrong plan was set on the subscription. Tax questions from EU clients regarding VAT were handled manually by the CFO.
  • After: Won -> subscription in Paddle in 8 seconds. The plan is taken from the deal’s custom field — no errors. EU VAT is automatic. On payment_failed — manager task on the same day.
  • Additionally: subscription.canceled -> custom field + CSM task -> retention outreach within 24 hours instead of “accidentally finding out a week later.”

Who This Is Relevant For

  • SaaS companies with clients in the EU, UK, and US — where correct VAT/sales tax on invoices is required
  • Teams already using Kommo as their primary CRM and choosing Paddle as their MoR
  • Products with multiple pricing plans — so the plan from the deal automatically goes to Paddle without manual selection
  • Companies that want to see payment history in the client card without switching between systems

For assessing custom Kommo integrations with payment systems — typical scope of work is 1–2 weeks: plan mapping, webhook handling, storing paddle_customer_id.

Frequently Asked Questions

How does Paddle differ from Stripe Billing?

Stripe is a payment processor: you are responsible for taxes. Paddle is a Merchant of Record: Paddle is the legal seller, it collects and remits taxes. For companies with EU clients this is a fundamental difference: Stripe requires your own EU VAT registration or connecting TaxJar/Avalara, while Paddle does it automatically. A Kommo + Stripe integration is described separately.

Does Paddle support one-time payments or only subscriptions?

Both. transaction.completed fires for both one-time payments (payment links) and recurring ones. For the Kommo integration, a one-time payment is handled the same way: find the deal by customer_id and write a Note.

How do I store paddle_customer_id in Kommo?

Create a custom “Text” field in Kommo settings -> Deals -> Fields. When creating a customer in Paddle, immediately save the ID via PATCH /api/v4/leads/{id}. This field is used to look up the deal from incoming webhook events.

What happens if the client already exists in Paddle?

Paddle returns a 409 error for a duplicate email. Before creating — do a GET /customers?search={email}. If the customer is found — update their data via PATCH /customers/{id} and create a new subscription for the existing customer.

How do I test without real payments?

Paddle provides a sandbox environment: https://sandbox-api.paddle.com. Sandbox keys are created in Paddle Dashboard -> Developer Tools -> Authentication. Webhook events can be triggered manually from the Paddle Dashboard in sandbox mode.

Summary

  • Paddle: Bearer auth, base URL https://api.paddle.com
  • Create customer: POST /customers, create subscription: POST /subscriptions
  • Store paddle_customer_id in a Kommo custom field for reverse lookup
  • Webhook verification: HMAC-SHA256 via Paddle-Signature header, ts:payload format
  • Key events: subscription.created, transaction.completed, transaction.payment_failed, subscription.canceled
  • MoR model: EU VAT, UK VAT, US sales tax — Paddle handles it all

If you use Paddle and Kommo and want to tie subscriptions to your CRM — describe your pricing structure and custom fields. Exceltic.dev will configure the mapping and webhook event handling.

More articles

All →