Kommo + Wise Business: автоматические выплаты подрядчикам при закрытии сделки

Wise Business API позволяет создавать международные переводы программно: выбрать получателя, указать сумму и валюту, подтвердить - и деньги уходят без ручного перехода в банк. Для агентств и консалтинговых компаний, которые платят подрядчикам из Kommo-воронки, интеграция закрывает стандартный разрыв: менеджер переводит сделку на этап “Оплатить”, и платёж создаётся автоматически.

Wise API использует Bearer token (API Key из Wise Business -> Developer Tools). Основной flow из нескольких вызовов: GET /v3/profiles -> POST /v3/quotes -> POST /v3/accounts -> POST /v3/transfers -> POST /v3/transfers/{id}/payments. Каждый шаг зависит от результата предыдущего - именно поэтому Zapier не справляется с этой задачей.

Wise Business - API-доступный сервис международных переводов с поддержкой 80+ валют и multi-currency балансом. Отличается от Wise Personal: Business-аккаунт требует верификации компании, открывает полный API и batch-платежи.

Почему Zapier не закрывает эту задачу

Wise-коннектор в Zapier поддерживает только простые одношаговые операции. Transfer flow требует минимум 4-5 API-вызовов в строгой последовательности: сначала получить profileId, потом создать quote (он привязан к конкретной сумме и курсу обмена), потом убедиться что получатель существует, потом создать transfer и отдельно его подтвердить. Ни один low-code инструмент не моделирует такой граф зависимостей без кастомного кода.

Типовой масштаб: агентство с 20-30 выплатами подрядчикам в месяц тратит 2-3 часа на ручные переводы. Интеграция сводит это к одному действию менеджера в Kommo.

Архитектура

Kommo: сделка -> этап "Оплатить подрядчика"
  -> Kommo webhook leads.status.changed
  -> Ваш сервер

Ваш сервер:
  1. GET /v3/profiles -> profileId
  2. POST /v3/profiles/{profileId}/quotes -> quoteId
  3. GET или POST /v3/accounts -> recipientAccountId (кэш в Kommo)
  4. POST /v3/transfers -> transferId
  5. POST /v3/profiles/{profileId}/transfers/{transferId}/payments -> отправить

  -> Kommo: note с transferId и статусом

RecipientAccountId кэшируется в кастомном поле Kommo-контакта. При повторных выплатах одному подрядчику шаги 3 пропускается.

Реализация

import requests, os, uuid
from flask import Flask, request, jsonify

app = Flask(__name__)

WISE_API_KEY     = os.environ["WISE_API_KEY"]
WISE_BASE        = "https://api.transferwise.com"
WISE_HDR         = {"Authorization": f"Bearer {WISE_API_KEY}",
                    "Content-Type": "application/json"}

KOMMO_SUBDOMAIN  = os.environ["KOMMO_SUBDOMAIN"]
KOMMO_TOKEN      = os.environ["KOMMO_ACCESS_TOKEN"]
PAYOUT_STAGE_ID  = int(os.environ["KOMMO_PAYOUT_STAGE_ID"])

KOMMO_BASE = f"https://{KOMMO_SUBDOMAIN}.kommo.com/api/v4"
KOMMO_HDR  = {"Authorization": f"Bearer {KOMMO_TOKEN}",
              "Content-Type": "application/json"}

CF_WISE_ACCOUNT_ID = int(os.environ["CF_WISE_ACCOUNT_ID"])
CF_PAYOUT_AMOUNT   = int(os.environ["CF_PAYOUT_AMOUNT"])
CF_PAYOUT_CURRENCY = int(os.environ["CF_PAYOUT_CURRENCY"])

def get_profile_id() -> int:
    r = requests.get(f"{WISE_BASE}/v3/profiles", headers=WISE_HDR)
    r.raise_for_status()
    for p in r.json():
        if p["type"] == "BUSINESS":
            return p["id"]
    raise ValueError("No BUSINESS profile")

def create_quote(profile_id: int, source_cur: str,
                 target_cur: str, amount: float) -> str:
    r = requests.post(
        f"{WISE_BASE}/v3/profiles/{profile_id}/quotes",
        headers=WISE_HDR,
        json={
            "sourceCurrency": source_cur,
            "targetCurrency": target_cur,
            "targetAmount":   amount,
            "payOut":         "BANK_TRANSFER",
        },
    )
    r.raise_for_status()
    return r.json()["id"]

def get_or_create_recipient(profile_id: int, contact: dict) -> int:
    existing = get_cf(contact, CF_WISE_ACCOUNT_ID)
    if existing:
        return int(existing)

    name  = contact.get("name", "")
    email = get_email(contact)
    r = requests.post(
        f"{WISE_BASE}/v3/accounts",
        headers=WISE_HDR,
        json={
            "currency":           "USD",
            "type":               "email",
            "profile":            profile_id,
            "accountHolderName":  name,
            "details":            {"email": email},
        },
    )
    r.raise_for_status()
    account_id = r.json()["id"]
    save_cf(contact["id"], CF_WISE_ACCOUNT_ID, str(account_id))
    return account_id

def create_transfer(quote_id: str, recipient_id: int, lead_id: int) -> int:
    r = requests.post(
        f"{WISE_BASE}/v3/transfers",
        headers=WISE_HDR,
        json={
            "targetAccount":         recipient_id,
            "quoteUuid":             quote_id,
            "customerTransactionId": str(uuid.uuid4()),
            "details":               {"reference": f"Kommo deal #{lead_id}"},
        },
    )
    r.raise_for_status()
    return r.json()["id"]

def fund_transfer(profile_id: int, transfer_id: int) -> str:
    r = requests.post(
        f"{WISE_BASE}/v3/profiles/{profile_id}/transfers/{transfer_id}/payments",
        headers=WISE_HDR,
        json={"type": "BALANCE"},
    )
    r.raise_for_status()
    return r.json().get("status", "unknown")

def get_cf(entity: dict, field_id: int) -> str:
    for cf in entity.get("custom_fields_values", []) or []:
        if cf.get("field_id") == field_id:
            vals = cf.get("values", [])
            return vals[0].get("value", "") if vals else ""
    return ""

def get_email(contact: dict) -> str:
    for cf in contact.get("custom_fields_values", []) or []:
        if cf.get("field_code") == "EMAIL":
            vals = cf.get("values", [])
            return vals[0].get("value", "") if vals else ""
    return ""

def save_cf(contact_id: int, field_id: int, value: str):
    requests.patch(
        f"{KOMMO_BASE}/contacts/{contact_id}",
        headers=KOMMO_HDR,
        json={"custom_fields_values": [
            {"field_id": field_id, "values": [{"value": value}]}
        ]},
    )

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}}],
    )

def get_lead_contact(lead_id: int) -> tuple:
    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

@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")
        if new_status != PAYOUT_STAGE_ID:
            continue

        lead, contact = get_lead_contact(lead_id)
        amount   = float(get_cf(lead, CF_PAYOUT_AMOUNT) or lead.get("price") or 0)
        currency = get_cf(lead, CF_PAYOUT_CURRENCY) or "USD"

        if amount <= 0:
            add_note(lead_id, "Wise: сумма выплаты не задана.")
            continue

        profile_id  = get_profile_id()
        quote_id    = create_quote(profile_id, "USD", currency, amount)
        recipient   = get_or_create_recipient(profile_id, contact)
        transfer_id = create_transfer(quote_id, recipient, lead_id)
        status      = fund_transfer(profile_id, transfer_id)

        add_note(lead_id,
                 f"Wise перевод #{transfer_id}: {amount} {currency}, статус: {status}")

    return jsonify({"status": "ok"}), 200

Webhook Wise для отслеживания статуса

Wise отправляет события о статусе перевода через подписки. Создать подписку:

def subscribe_wise_webhooks(profile_id: int,
                             callback_url: str, secret: str) -> str:
    r = requests.post(
        f"{WISE_BASE}/v3/subscriptions",
        headers=WISE_HDR,
        json={
            "name":       "Kommo transfer updates",
            "trigger_on": "transfers#state-change",
            "delivery": {
                "version": "2.0",
                "url":     callback_url,
                "signature": {
                    "type":  "SHA256_HMAC",
                    "token": secret,
                },
            },
            "scope": {
                "domain": "profile",
                "id":     str(profile_id),
            },
        },
    )
    r.raise_for_status()
    return r.json()["id"]

@app.route("/webhooks/wise", methods=["POST"])
def wise_webhook():
    import hmac, hashlib
    secret   = os.environ["WISE_WEBHOOK_SECRET"]
    sig      = request.headers.get("X-Signature-SHA256", "")
    body     = request.get_data()
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(sig, expected):
        return jsonify({"error": "invalid signature"}), 401

    event = request.json or {}
    if event.get("event_type") != "transfers#state-change":
        return jsonify({"status": "ignored"}), 200

    resource  = event.get("data", {}).get("resource", {})
    t_id      = resource.get("id")
    new_state = resource.get("current_state")
    # outgoing_payment_sent = деньги отправлены
    # funds_refunded = возврат
    print(f"Transfer {t_id} -> {new_state}")
    return jsonify({"status": "ok"}), 200

Ключевые состояния: processing, funds_converted, outgoing_payment_sent, funds_refunded.

Sandbox для тестирования

Wise предоставляет sandbox: sandbox.transferwise.com. Создайте отдельный ключ в Wise Business -> Developer Tools -> Sandbox. Все переводы в sandbox не требуют реальных средств и можно протестировать весь flow, включая webhook-события.

Batch-выплаты

Для 10+ выплат в день используйте POST /v3/profiles/{profileId}/batch-payments - один запрос с массивом transferId вместо N отдельных payment-вызовов. Transfer’ы создаются по одному (каждый с уникальным customerTransactionId), а подтверждаются одним batch-запросом.

Для кого актуально

Агентства и консалтинговые компании с международными подрядчиками: дизайнерами в EU, разработчиками в UK/Canada/Australia, фрилансерами по всему миру. Типовой сценарий: проект выигран в воронке Kommo, сделка закрыта, нужно выплатить гонорар - без перехода в банк и без ручной работы. Особенно актуально при 15+ выплатах в месяц, когда ручная работа становится систематической потерей времени.

Альтернативные платёжные интеграции: Kommo + Razorpay (India), Kommo + Flutterwave (Африка).

Часто задаваемые вопросы

Можно ли создавать переводы без ручного подтверждения?

Да. Шаг POST .../transfers/{id}/payments с {"type": "BALANCE"} подтверждает перевод автоматически. Предварительное условие: достаточный баланс в исходной валюте. Добавьте проверку баланса через GET /v4/profiles/{profileId}/balances перед созданием transfer.

Как хранить wise_account_id подрядчика в Kommo?

Создайте кастомное поле типа “Текст” в разделе Kontakты Kommo. При первом создании перевода запишите recipientAccountId в это поле через PATCH /api/v4/contacts/{id}. При последующих выплатах тому же подрядчику - читайте поле и пропускайте шаг POST /v3/accounts.

Что если на балансе Wise недостаточно средств?

Wise вернёт HTTP 422 с кодом INSUFFICIENT_BALANCE на шаге payments. Обработайте это: поймайте ошибку, добавьте note в Kommo “Wise: пополните баланс для перевода {amount} {currency}” и выйдите без crash. Мониторинг баланса - отдельная задача через Wise API или уведомления в Wise Dashboard.

Поддерживает ли Wise transfers на криптокошельки?

Нет. Wise - только банковские переводы, IBAN, SWIFT, местные схемы (ACH, SEPA, Faster Payments). Для крипто-выплат нужен отдельный сервис (Coinbase Commerce, BitPay).

Итог

Kommo + Wise Business - автоматические выплаты подрядчикам:

  • Bearer token, обязательный flow: profile -> quote -> account -> transfer -> payment
  • Кэшировать recipientAccountId в кастомном поле Kommo-контакта
  • Webhook transfers#state-change через SHA256_HMAC-подписку
  • Sandbox: sandbox.transferwise.com для полного тестирования без реальных денег
  • 80+ валют, mid-market rate, прозрачная комиссия

Если ваша команда платит международным подрядчикам через Kommo - опишите задачу команде Exceltic.dev. Разберём архитектуру под ваш стек.

Ещё статьи

Все →