Adyen is an enterprise payment platform used by Uber, Spotify, McDonald’s, and Booking.com: a unified gateway for online payments, in-store (POS), in-app, and marketplace payouts. Its key advantages over Stripe or Recurly include global acquiring (direct agreements with payment networks without intermediaries), built-in reconciliation via Adyen Data for financial reporting, and marketplace split payments. For B2B companies with enterprise clients, Adyen is often a contractual standard — not because it’s simpler, but because the contract requires it. Without integration with Kommo Won: payment links are created manually, and managers can’t see payment status in the CRM.
Adyen vs Stripe vs Mollie for enterprise B2B
| Parameter | Adyen | Stripe | Mollie |
|---|---|---|---|
| Acquiring model | Direct (own acquiring) | Through partners | Through partners |
| Interchange++ pricing | Yes | No | No |
| Marketplace payments | Native | Stripe Connect | No |
| In-store (POS) | Yes (Adyen Terminal) | Stripe Terminal | No |
| Minimum volume | ~$1M/year | None | None |
| Reconciliation/reporting | Adyen Data (built-in) | Via dashboard | Via dashboard |
| EU orientation | Yes (Amsterdam) | Yes (Dublin) | Yes (Netherlands) |
Companies with over $1M/year in volume choose Adyen when direct interchange++ pricing matters and a unified platform for all sales channels is required.
Architecture: Kommo + Adyen
Kommo Won -> Python handler -> Adyen Payment Links API -> link in Note
Adyen Webhook -> Python handler -> Kommo Note / Deal Stage
Adyen has no connector for Kommo. Integration is built via Adyen API (Management API + Checkout API) and webhooks.
Authentication: Adyen API Key
Adyen uses the X-API-Key header. The API Key is created in Adyen Customer Area -> Developers -> API credentials -> Create new credential.
import requests
import hmac, hashlib, base64
ADYEN_API_KEY = "your_api_key"
ADYEN_MERCHANT = "YourMerchantAccount"
ADYEN_BASE = "https://checkout-test.adyen.com/checkout/v71"
# Production: https://checkout-live.adyen.com/checkout/v71
ADYEN_HEADERS = {
"X-API-Key": ADYEN_API_KEY,
"Content-Type": "application/json",
}
ADYEN_HMAC_KEY = "your_hmac_key_hex" # from Adyen Customer Area -> Webhooks
Creating a payment link on Won
Adyen Payment Links allow you to create a hosted checkout link without PCI responsibility on your side:
from datetime import datetime, timezone, timedelta
def create_adyen_payment_link(amount_cents: int, currency: str,
description: str, reference: str,
email: str = "") -> dict:
# reference - unique identifier, use Kommo deal ID
expires_at = (
datetime.now(timezone.utc) + timedelta(days=7)
).strftime("%Y-%m-%dT%H:%M:%S+00:00")
payload = {
"merchantAccount": ADYEN_MERCHANT,
"amount": {
"currency": currency,
"value": amount_cents,
},
"reference": reference,
"description": description,
"expiresAt": expires_at,
"returnUrl": "https://yoursite.com/payment-complete",
}
if email:
payload["shopperEmail"] = email
resp = requests.post(
f"{ADYEN_BASE}/paymentLinks",
headers=ADYEN_HEADERS,
json=payload,
)
resp.raise_for_status()
return resp.json()
def on_kommo_deal_won(lead: dict, contact: dict):
price = int((lead.get("price") or 0) * 100) # cents
currency = get_custom_field(lead, CURRENCY_FIELD_ID) or "EUR"
reference = f"kommo-{lead['id']}"
description = lead.get("name", "")
email = get_contact_email(contact)
link_data = create_adyen_payment_link(
amount_cents=price,
currency=currency,
description=description,
reference=reference,
email=email,
)
link_url = link_data.get("url", "")
link_id = link_data.get("id", "")
save_to_kommo_deal(lead["id"], {
"adyen_payment_link_id": link_id,
"adyen_reference": reference,
})
create_kommo_note(
lead["id"],
f"Adyen: payment link created -> {link_url} (expires in 7 days)",
)
Webhook: Adyen -> Kommo
Adyen webhooks use HMAC-SHA256 signing. Setup: Customer Area -> Developers -> Webhooks -> Standard webhook.
def verify_adyen_hmac(payload: dict, hmac_key_hex: str,
received_hmac: str) -> bool:
# Adyen HMAC verification
fields = [
"pspReference", "originalReference", "merchantAccountCode",
"merchantReference", "value", "currency", "eventCode", "success",
]
hmac_key = bytes.fromhex(hmac_key_hex)
data = ":".join(str(payload.get(f, "")) for f in fields)
computed = base64.b64encode(
hmac.new(hmac_key, data.encode("utf-8"), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(computed, received_hmac)
@app.route("/webhooks/adyen", methods=["POST"])
def adyen_webhook():
body = request.json
for item in body.get("notificationItems", []):
notification = item.get("NotificationRequestItem", {})
received_hmac = notification.get("additionalData", {}).get("hmacSignature", "")
if not verify_adyen_hmac(notification, ADYEN_HMAC_KEY, received_hmac):
return "[accepted]", 401
event_code = notification.get("eventCode", "")
reference = notification.get("merchantReference", "")
success = notification.get("success") == "true"
lead_id = find_kommo_deal_by_custom_field("adyen_reference", reference)
if not lead_id:
continue
if event_code == "AUTHORISATION" and success:
amount = notification.get("amount", {})
value_str = f"{amount.get('value', 0) / 100:.2f} {amount.get('currency', '')}"
create_kommo_note(lead_id,
f"Adyen: payment authorized - {value_str}")
elif event_code == "CAPTURE" and success:
amount = notification.get("amount", {})
value_str = f"{amount.get('value', 0) / 100:.2f} {amount.get('currency', '')}"
create_kommo_note(lead_id,
f"Adyen: payment captured - {value_str}")
move_kommo_deal_to_stage(lead_id, PAID_STAGE_ID)
elif event_code == "REFUND" and success:
create_kommo_note(lead_id, "Adyen: refund processed")
elif event_code == "CHARGEBACK":
create_kommo_note(lead_id, "Adyen: chargeback - action required")
create_kommo_task(lead_id, "Adyen: handle chargeback")
return "[accepted]", 200
Important: Adyen requires the response body [accepted] (a string) with HTTP 200, otherwise it retries the webhook up to 10 times.
Marketplace split payments
If you have a marketplace model (selling on behalf of multiple vendors), Adyen supports automatic payment splitting via the splits object:
def create_split_payment_link(amount_cents: int, currency: str,
reference: str, splits: list) -> dict:
payload = {
"merchantAccount": ADYEN_MERCHANT,
"amount": {"currency": currency, "value": amount_cents},
"reference": reference,
"splits": splits,
}
resp = requests.post(
f"{ADYEN_BASE}/paymentLinks",
headers=ADYEN_HEADERS,
json=payload,
)
resp.raise_for_status()
return resp.json()
# splits example: 90% to vendor, 10% marketplace commission
SPLITS_EXAMPLE = [
{"type": "MarketPlace", "amount": {"value": 9000}, "account": "vendor_account_code"},
{"type": "Commission", "amount": {"value": 1000}},
]
Real-world case study
B2B SaaS marketplace (EU, 200+ enterprise clients, Kommo + Adyen):
- Before: Won -> finance team manually created a payment request in Adyen Customer Area and emailed the link. 1–2 business days of delay. Manager couldn’t see payment status without logging into Adyen.
- After: Won -> Python webhook -> Adyen Payment Link in 2 seconds -> link in Note.
CAPTUREevent -> Note “paid” + deal stage moved to “Paid.” Manager sees the full cycle in Kommo without opening Adyen. - Additionally:
CHARGEBACKevent -> Note + Task -> manager responds the same day. Before integration, chargeback notifications only reached the CFO by email.
Who benefits from this integration
- Enterprise B2B with Adyen as a contractual standard (the contract specifies a particular gateway)
- Marketplace companies that need automatic split payments between vendors and the platform
- Companies with multi-currency sales in EU/US/APAC — Adyen supports 150+ currencies natively
- Teams where sales managers need to see payment status in the CRM without access to Adyen Customer Area
Frequently asked questions
Adyen test vs production — how to switch?
Adyen provides a test environment (Customer Area Test) and production (Customer Area Live). URLs differ: checkout-test.adyen.com vs checkout-live.adyen.com. API Keys are different for test and live. In code: use an environment variable ADYEN_ENV = "test" | "live" to switch the base URL and API key.
Adyen Payment Links — expiration and reuse?
A Payment Link is created with an expiresAt parameter (ISO 8601). Once expired, the link is invalid. To request payment again, create a new Link via POST /paymentLinks. Check existing Link status: GET /paymentLinks/{id} — the status field can be active, expired, or completed. On expired -> automatically create a new one via webhook or cron job.
How does Adyen handle EU Strong Customer Authentication (SCA)?
Adyen natively supports 3DS2 for SCA. Payment Links automatically include a 3DS challenge when required by the client’s bank. No additional configuration is needed — Adyen determines whether a challenge is necessary through an AI model (exemptions: transaction risk analysis). For B2B payments with corporate cards, SCA is often exempt through a TRA (Transaction Risk Analysis) exemption.
Adyen webhooks vs Adyen Reports — which to use for reconciliation?
Webhooks: real-time events (AUTHORISATION, CAPTURE, REFUND). Adyen Reports: batch files (CSV) available daily via SFTP or Adyen Reporting API for accounting reconciliation. For Kommo integration — use webhooks. For ERP/accounting — use Reports API. Both can be used in parallel.
Summary
- Auth:
X-API-Keyheader, not Bearer token - Payment Link:
POST /checkout/v71/paymentLinks, reference = Kommo deal ID - Webhook: respond with
[accepted](string), HMAC-SHA256 verification is mandatory - Key events:
CAPTURE(funds received),CHARGEBACK(task for manager),REFUND - Adyen split payments for marketplace:
splitsobject in payload
If you use Adyen and Kommo and want payment status visible in the deal card — describe your payment model (single payments or marketplace). Exceltic.dev will set up two-way integration with HMAC verification.