Developer guide
Why the REST API instead of iyzipay-php
Section titled “Why the REST API instead of iyzipay-php”iyzico’s official PHP client (iyzipay-php) is MIT-licensed and could be bundled. We call the REST API directly instead:
- The add-on uses ten endpoints. A small client (about 400 lines) covers them, while iyzipay-php is more than 300 classes.
- TLS verification stays on and we control timeouts. The core
Tygh\Httpturns peer verification off. - Logging and masking are under our control. Nothing is logged by a third-party layer.
- No global autoloader collisions with other extensions that bundle a different iyzipay-php version.
- PHP 7.2–8.3 compatibility is in our hands. Newer iyzipay-php releases use PHP 8 type declarations.
The signing code follows Iyzipay\IyziAuthV2Generator, and the unit tests compare our output with vectors produced by the official client.
app/addons/kv_iyzico_payments/ addon.xml, init.php, func.php settings, tables, hook handlers, install/uninstall payments/kv_iyzico_payments.php processor script: payment start + payment_notification modes controllers/frontend/kv_iyzico_payments.php pay (embedded form page), installments (AJAX) controllers/backend/kv_iyzico_payments.php refund, cancel, approve, check_status, sync_submerchant, test_connection, payout_changes (admin list), approve/reject_payout_change (admin), cancel_payout_change (vendor) controllers/backend/orders.post.php "iyzico" tab on orders.details controllers/backend/companies.post.php "iyzico sub-merchant" tab on companies.update controllers/backend/kv_iyzico_payments_license.php "Check now" / cron license check schemas/permissions/admin.post.php, vendor_multivendor.post.php schemas/menu/menu_multivendor.post.php Vendors > iyzico payout changes (administrators) Tygh/Addons/KvIyzicoPayments/ Api/Client.php REST client (IYZWSv2), one method per endpoint Api/Signer.php request auth, response / webhook signature checks Api/Money.php minor-unit math, iyzico price format ("12.5", "100.0") Api/Masker.php log masking Api/CurlTransport.php, TransportInterface.php, ApiException.php Basket/BasketBuilder.php, Allocator.php exact basket math, marketplace split OrderMapper.php order -> initialize request (buyer, addresses, lines, currency) PaymentService.php initialize, complete (idempotent), refund, cancel Installments.php installment table Marketplace/Marketplace.php vendor ratios, sub-merchant sync, approvals Marketplace/PayoutChanges.php admin approval of vendor payout detail changes (pending changes, masking, notifications) Repository.php, Config.php, Logger.php License/Client.php license check client (generated from the shared Kavio Labs template) Upgrade/Synchronizer.php self-healing in-place upgradesdesign/backend/templates/addons/kv_iyzico_payments/license/info.tpl License tabPayment flow
Section titled “Payment flow”-
fn_start_payment()includespayments/kv_iyzico_payments.php.PaymentService::initialize()builds the request withOrderMapperand callsPOST /payment/iyzipos/checkoutform/initialize/auth/ecom(or/payment/pay-with-iyzico/initialize). It verifies the response signature (conversationId:token) and stores a transaction (status = pending, token, random access key, basket map). Then it redirects to the hosted page or tokv_iyzico_payments.pay?order_id=&key=&pk=. Payment page access: thekey(random access key of the transaction) and the visitor who placed the order: the logged-in owner, a guest session with the order inauth.order_ids, or thepkpage key.pkisApi\Signer::pageKey(), an HMAC of the order, the token and the session ID with the method’s secret key. A checkout request of the same visitor still running when the order is placed (e.g. the address update fired while a guest types) can finish last and save its stale session copy, which drops the guest’sauth.order_ids; a validpkrestores it (PaymentService::canOpenPaymentPage()/grantGuestAccess()). The key is never sent to iyzico, is useless in any other session and never opens another order. -
The callback URL is
payment_notification.return?payment=kv_iyzico_payments&order_id=N&sid_customer_…=<session>. iyzico posts the callback cross-site, where the SameSite=Lax session cookie is not sent, so the session ID travels in the URL. The core PayPal add-on does the same. The callback runs in the session that started the payment and the token proves the payer, so it restores a guest’s lost order access (PaymentService::grantGuestAccess(), guest orders only) before routing tofinish; otherwise the guest would land on the login form instead of the order confirmation. -
PaymentService::complete($token)runs:- It claims the transaction atomically (
UPDATE … SET status='processing' WHERE status IN ('pending', …)). A second, concurrent callback or webhook getsprocessingand does nothing. - It calls
POST /payment/iyzipos/checkoutform/auth/ecom/detail. - It verifies the signature over
paymentStatus:paymentId:currency:basketId:conversationId:paidPrice:price:token. The decimal fields are tried in each common format (120.5,120.50). - It checks the basket ID, currency and amounts against the stored transaction.
- It stores the item transactions and applies the installment surcharge if configured.
- It calls
fn_finish_payment(). If the payment-started marker is already gone, it updates the status only while the order is still waiting for payment.
A response without a final
paymentStatus(the customer is still on the form) leaves the transaction pending. - It claims the transaction atomically (
-
payment_notification.finish(top-level, also used to leave the iframe) callsfn_order_placement_routines('route'). -
Webhook:
payment_notification.webhookchecksX-Iyz-Signature-V3= HMAC-SHA256(secret + eventType + iyziPaymentId + token + conversationId + status) and then callscomplete().
Tables
Section titled “Tables”| Table | Purpose |
|---|---|
kv_iyzico_payments_transactions |
One row per payment attempt: token, access key, conversation/basket ID, status (pending, processing, success, review, failed, cancelled, superseded), currency, price, paid price, installments, iyzico paymentId, basket map (JSON) |
kv_iyzico_payments_items |
iyzico item transactions: paymentTransactionId, vendor order, paid / refunded amounts, sub-merchant price, approval status |
kv_iyzico_payments_refunds |
Refund and cancel history |
kv_iyzico_payments_submerchants |
Vendor sub-merchant data and key, sync status |
kv_iyzico_payments_payout_changes |
Vendor payout detail change requests: vendor, status (pending, approved, rejected, cancelled, superseded), old and new values (JSON), who asked and when, who decided and when, reason, result. Created on install, and on first use when the add-on is updated from 1.0.1 or older without a reinstall (PayoutChanges::ensureTable()) |
Payout detail approval
Section titled “Payout detail approval”fn_kv_iyzico_payments_update_company() handles the posted company_data[kv_iyzico_payments]:
- Vendor panel (
runtime.company_idis the vendor) andPayoutChanges::isRequired():PayoutChanges::submit()normalizes the data, compares it with the live record (diff()), validates it (Marketplace::validate()) and stores apendingrow. An older pending row of the vendor becomessuperseded. The live row and iyzico are not touched. - Otherwise (administrator, or approval off): saved to the live row and synced right away, as in 1.0.1.
PayoutChanges::approve() builds the live row from the pending values and calls Marketplace::pushRow($company_id, $row, false). On success the row is stored as live and synced. On failure nothing is stored and the change stays pending with the error in result.
isRequired() uses decide() (pure, unit tested): always (the default, and any unknown value) means yes; never means no; premoderation means vendor_data_premoderation is active and vendor_profile_updates_approval is all, or custom with companies.pre_moderation_edit_vendors = Y.
Notifications: the Notifications center tag kv_iyzico_payout keeps one unread “waiting” entry per root administrator (refreshed on every change). The tag kv_iyzico_payout_decision is used for the vendor’s decision entries. E-mails go through Tygh::$app['mailer'] with a plain subject and body (no mail templates).
The vendor_data_premoderation_diff_company_data_post handler from 1.0.1 still removes kv_iyzico_payments from the profile diff, so this data never changes the vendor status.
The add-on handles save_log, prepare_checkout_payment_methods (hides unusable methods, and every iyzico method without a valid license), change_order_status_post (marketplace auto-approval), update_company (vendor data: sync, or a pending payout change), delete_company (removes the vendor’s sub-merchant data and change requests) and vendor_data_premoderation_diff_company_data_post (the iyzico data never counts as a vendor profile change).
The license client adds a static before_dispatch handler (License\Client::onBeforeDispatch) for the admin area and the console only: it runs the due license check and shows the license notices.
It also provides one hook:
/** * Allows changing the marketplace fee kept from a vendor order (primary currency). * @param int $company_id Vendor ID * @param int $order_id Vendor order ID * @param float $total Vendor order total * @param float $fee Marketplace fee (by reference) */fn_set_hook('kv_iyzico_payments_get_vendor_fee', $company_id, $order_id, $total, $fee);License check
Section titled “License check”Tygh\Addons\KvIyzicoPayments\License\Client verifies the license against the Kavio Labs license server.
fn_kv_iyzico_payments_is_licensed()is the only gate. When it returnsfalse,fn_kv_iyzico_payments_prepare_checkout_payment_methods()removes every iyzico payment method from the checkout list. It reads the stored state only (one storage read per request) and never contacts the server.- Nothing that completes an already started payment depends on the license: the processor script (
payments/kv_iyzico_payments.php), thekv_iyzico_payments.paypage, the callback and the webhook (payment_notification.*), refunds, cancels, status checks, item approvals (change_order_status_post) and the sub-merchant sync work for existing orders whatever the license state. Client::boot()(init.php) registers abefore_dispatchhandler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.- Admin license UI (generated): the License card at the top of the add-on page (
license/card.tplthrough theaddons:tabs_contentpre hook), the badge in Add-ons > Manage (license/badge.tpl,addons:managepost hook), the status bar (license/status_bar.tpl, first line inside the mainbox capture ofviews/kv_iyzico_payments/payout_changes.tpl; nothing for vendors) andcss/addons/kv_iyzico_payments/license.less(index:stylespost hook). The off-effect sentence is the language variablekv_iyzico_payments.license_off_effect. - Request:
POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.checkwith JSON{"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent throughTygh\Http(3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce. - State: storage key
kv_license_kv_iyzico_payments(?:storage_data), orvar/kv_storage/kv_iyzico_payments/license.jsonwhile the full page cache is active, protected by an HMAC with the store’s crypt key. - Cron:
php admin.php --dispatch=kv_iyzico_payments_license.check. - Development stores only (
http_hostandhttps_hostare development hosts): the constantsKV_LICENSE_SERVER_URL,KV_LICENSE_TEST_HOST(act as if the development storefronts ran on that host) andKV_LICENSE_TEST_TIME_OFFSET(seconds). They are ignored on production hosts.
Local development without iyzico credentials
Section titled “Local development without iyzico credentials”tests/mock/router.php is a mock of the iyzico endpoints the add-on uses. It checks IYZWSv2 authentication exactly like iyzico, validates basket sums and sub-merchant prices, and returns the documented response shapes with valid signatures. It also has a hosted payment page with Pay, Pay in 3 installments, Fraud review and Decline buttons.
php81 -S 127.0.0.1:8199 addons/iyzico_payments/tests/mock/router.phpPoint a development store at the mock with a constant in config.local.php or local_conf.php:
define('KV_IYZICO_PAYMENTS_BASE_URL', 'http://127.0.0.1:8199');Use the keys sandbox-mock-api-key / sandbox-mock-secret-key. The constant overrides the API base URL for both modes. Never define it on a production store.
Tests:
php81 addons/iyzico_payments/tests/unit/run.php # signing, basket math, masking, clientphp81 addons/iyzico_payments/tests/mock/selftest.php # API contract against the mockphp81 addons/iyzico_payments/tests/mock/selftest.php https://sandbox-api.iyzipay.com <key> <secret> # real sandbox (stops before payment)bash addons/iyzico_payments/tests/e2e/run.sh http://localhost:8171 cscart_cs417 # storefront + admin, headless Chromebash addons/iyzico_payments/tests/e2e/run_mv.sh http://localhost:8172 cscart_mv417 # marketplace mode on Multi-VendorSecurity notes
Section titled “Security notes”- Card data never reaches the store. All display modes use iyzico’s form or page.
- A callback is trusted only after a server-side retrieve with the merchant credentials, a signature check and an amount check. The token must belong to the order in the URL.
- The pay page and the customer cancel link need a random 128-bit key bound to the transaction.
- The license check endpoint (
kv_iyzico_payments_license.check) accepts only CSRF-protected POST requests of administrators, and console requests. Vendors cannot use it. - Refund, cancel and approval actions are admin-only: the
edit_orderpermission, and vendors are excluded. Vendors may only resend their own (approved) sub-merchant data and withdraw their own pending payout change. - Payout detail changes by vendors wait for an administrator by default (
manage_vendorsto approve or reject,view_vendorsto see the list). The pending box and the list mask IBANs, identity and tax numbers and phones. Full values are shown only in the admin list, behind Show full values. - Logs mask identity numbers, e-mails, phones, IBANs, names, addresses, tokens and keys.
checkoutFormContentis never logged. - The secret key never appears in a response, a log or a template. The admin form shows it in a password field.
Third-party code
Section titled “Third-party code”Tygh/Addons/KvIyzicoPayments/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.