Skip to content

Developer guide

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\Http turns 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 upgrades
design/backend/templates/addons/kv_iyzico_payments/license/info.tpl License tab
  1. fn_start_payment() includes payments/kv_iyzico_payments.php. PaymentService::initialize() builds the request with OrderMapper and calls POST /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 to kv_iyzico_payments.pay?order_id=&key=&pk=. Payment page access: the key (random access key of the transaction) and the visitor who placed the order: the logged-in owner, a guest session with the order in auth.order_ids, or the pk page key. pk is Api\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’s auth.order_ids; a valid pk restores it (PaymentService::canOpenPaymentPage() / grantGuestAccess()). The key is never sent to iyzico, is useless in any other session and never opens another order.

  2. 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 to finish; otherwise the guest would land on the login form instead of the order confirmation.

  3. PaymentService::complete($token) runs:

    • It claims the transaction atomically (UPDATE … SET status='processing' WHERE status IN ('pending', …)). A second, concurrent callback or webhook gets processing and 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.

  4. payment_notification.finish (top-level, also used to leave the iframe) calls fn_order_placement_routines('route').

  5. Webhook: payment_notification.webhook checks X-Iyz-Signature-V3 = HMAC-SHA256(secret + eventType + iyziPaymentId + token + conversationId + status) and then calls complete().

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())

fn_kv_iyzico_payments_update_company() handles the posted company_data[kv_iyzico_payments]:

  • Vendor panel (runtime.company_id is the vendor) and PayoutChanges::isRequired(): PayoutChanges::submit() normalizes the data, compares it with the live record (diff()), validates it (Marketplace::validate()) and stores a pending row. An older pending row of the vendor becomes superseded. 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);

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 returns false, 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), the kv_iyzico_payments.pay page, 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 a before_dispatch handler 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.tpl through the addons:tabs_content pre hook), the badge in Add-ons > Manage (license/badge.tpl, addons:manage post hook), the status bar (license/status_bar.tpl, first line inside the mainbox capture of views/kv_iyzico_payments/payout_changes.tpl; nothing for vendors) and css/addons/kv_iyzico_payments/license.less (index:styles post hook). The off-effect sentence is the language variable kv_iyzico_payments.license_off_effect.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\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), or var/kv_storage/kv_iyzico_payments/license.json while 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_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_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.php

Point 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, client
php81 addons/iyzico_payments/tests/mock/selftest.php # API contract against the mock
php81 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 Chrome
bash addons/iyzico_payments/tests/e2e/run_mv.sh http://localhost:8172 cscart_mv417 # marketplace mode on Multi-Vendor
  • 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_order permission, 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_vendors to approve or reject, view_vendors to 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. checkoutFormContent is never logged.
  • The secret key never appears in a response, a log or a template. The admin form shows it in a password field.

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.