Services Hosting & Servers Tools Blog Search Company TürkçeTR
Get a Quote

Official iyzipay SDKs handle request signing for you — but sometimes you want to integrate without adding a dependency, on shared hosting where Composer isn't available, or simply to understand exactly what's being sent over the wire. This guide walks through an open-source, SDK-free PHP + cURL integration end to end: the IYZWSv2 signature scheme, and the NON-3DS, 3D Secure, Checkout Form and PWI flows.

Related reading: iyzico Node.js integration guide · REST API security guide

The Open-Source Sample Project

Every code sample in this guide comes from github.com/EgemenKEYDAL/Iyzico-Payment-Samples — copy-paste-ready PHP code for NON-3DS, 3DS, PWI, Checkout Form, BIN lookup and webhooks, fully commented. Clone it and test directly with your own apiKey/secretKey.

IYZWSv2: Building the Signature by Hand

Without the SDK, you build the Authorization header yourself for every request. iyzico's IYZWSv2 scheme works like this: generate a random randomKey, concatenate randomKey + uri + JSON-encoded request body, run that through HMAC-SHA256 with your secretKey, then assemble apiKey:...&randomKey:...&signature:... and Base64-encode it.

public function generateAuthorizationHeader($uri, $requestBody = [])
{
    $randomKey = (string)(microtime(true) * 10000) . rand(100000, 999999);
    $jsonBody = empty($requestBody) ? '' : json_encode($requestBody, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

    $dataToEncrypt = $randomKey . $uri . $jsonBody;
    $signature = hash_hmac('sha256', $dataToEncrypt, $this->secretKey);

    $authString = "apiKey:" . $this->apiKey .
                  "&randomKey:" . $randomKey .
                  "&signature:" . $signature;

    return base64_encode($authString);
}

// Request header:
// Authorization: IYZWSv2 <base64-encoded-auth-string>
Warning
randomKey must be unique per request (timestamp + a random number is enough). Reusing it or hardcoding a fixed value defeats the entire purpose of the signature.

Four Payment Methods: Which One to Pick

MethodFlowWhen to Use
NON-3DSSingle request, no 3DS stepLow-value, low-risk transactions
3DSTwo-step: initialize → auth, requires bank OTPTransactions where you want to reduce chargeback risk
Checkout Form4 display modes (redirect/responsive/popup/iframe), card data never touches your serverCustomizable flows, smaller PCI scope
PWI (Pay With iyzico)One-click payment via iyzico account, redirect onlyBuyer protection program

NON-3DS Payment

The simplest flow: card details plus buyer and basket info go in a single /payment/auth request, and the response comes back immediately.

$paymentRequest = [
    'locale' => 'tr',
    'conversationId' => 'order-' . uniqid(),
    'price' => '100.00',
    'paidPrice' => '100.00',
    'currency' => 'TRY',
    'installment' => 1,
    'basketId' => 'B' . time(),
    'paymentChannel' => 'WEB',
    'paymentGroup' => 'PRODUCT',
    'paymentCard' => [
        'cardHolderName' => $cardHolderName,
        'cardNumber' => $cardNumber,
        'expireMonth' => $expireMonth,
        'expireYear' => $expireYear,
        'cvc' => $cvc,
        'registerCard' => 0
    ],
    'buyer' => $buyer,
    'shippingAddress' => $shippingAddress,
    'billingAddress' => $billingAddress,
    'basketItems' => $basketItems
];

$response = $iyzico->sendRequest('/payment/auth', $paymentRequest);

if ($response['status'] == 'success') {
    // store paymentId, approve the order
}

The 3D Secure Flow: Initialize + Auth

3DS payments split into two requests. The initialize step returns a Base64 threeDSHtmlContent — this HTML contains the bank's OTP form and must be rendered directly in the browser:

// callback.php — after the bank redirect
$authRequest = [
    'locale' => 'tr',
    'conversationId' => 'auth-' . uniqid(),
    'paymentId' => $_POST['paymentId'],
    'conversationData' => $_POST['conversationData'] ?? ''
];

$authResponse = $iyzico->sendRequest('/payment/3dsecure/auth', $authRequest);

if ($authResponse['status'] == 'success' && $authResponse['fraudStatus'] == 1) {
    // payment is final
}

mdStatus = 1 indicates a successful 3DS verification; any other value means the bank-side verification wasn't completed, and the auth request will fail.

Checkout Form: Four Display Modes

Initializing the Checkout Form gives you a token + paymentPageUrl, plus an embeddable checkoutFormContent (Base64 script). You can use it four different ways:

  • Redirect (most common) — header('Location: ' . $paymentPageUrl);
  • Responsive — embedded in your page: <div id="iyzipay-checkout-form" class="responsive"></div> plus the script
  • Popup — same script, class="popup" opens it as a popup window
  • iFrame<iframe src="{paymentPageUrl}&iframe=true">

The token is valid for 30 minutes. If the user doesn't complete payment in that window, you'll need to re-initialize. On callback, you retrieve the result using the token:

$retrieveRequest = [
    'locale' => 'tr',
    'conversationId' => 'cf-retrieve-' . uniqid(),
    'token' => $_POST['token']
];

$result = $iyzico->sendRequest('/payment/iyzipos/checkoutform/auth/ecom/detail', $retrieveRequest);

if ($result['status'] == 'success' && $result['paymentStatus'] == 'SUCCESS' && $result['fraudStatus'] == 1) {
    // approve the order
}

Verifying the Response Signature

Signing your request isn't enough — you also need to verify the response actually came from iyzico. The parameter order varies per endpoint; for the Checkout Form result, for example, it's paymentStatus, paymentId, currency, basketId, conversationId, paidPrice, price, token. Parameters are joined with :, hashed with the same secretKey via HMAC-SHA256, and compared with hash_equals() for constant-time safety:

public function verifyResponseSignature($response, $parameterOrder)
{
    $params = [];
    foreach ($parameterOrder as $param) {
        if (isset($response[$param])) {
            $value = $response[$param];
            if (in_array($param, ['price', 'paidPrice'])) {
                $value = rtrim(rtrim((string)$value, '0'), '.'); // strip trailing zeros
            }
            $params[] = $value;
        }
    }
    $dataToEncrypt = implode(':', $params);
    $calculated = hash_hmac('sha256', $dataToEncrypt, $this->secretKey);

    return hash_equals($calculated, $response['signature']);
}
Warning
Trailing zeros in price and paidPrice (10.5010.5) must be stripped before hashing, or the signature won't match even for a legitimate response. This is the single most common cause of "invalid signature" errors.

Checking Fraud Status and 3DS Requirement

Before shipping a product, always check fraudStatus — even when the bank approves a transaction instantly, iyzico's fraud engine may still flag it for review.

fraudStatusMeaningAction
1ApprovedSafe to ship the product
0Under reviewWait, it resolves automatically
-1RejectedDo not ship, cancel the order

Similarly, the BIN lookup response (/payment/bin/check) includes a force3ds field that tells you whether the card requires 3DS — when it's 1, only the 3DS flow works and a NON-3DS request will be rejected.

Webhooks: Event Types and Idempotency

iyzico POSTs a notification to your server once payment completes, retrying every 10 minutes (up to 3 times) until your server responds with 2xx. The iyziEventType field tells you which payment method triggered it:

iyziEventTypePayment Method
API_AUTHNON-3DS payment
THREE_DS_AUTH3DS payment
CHECKOUTFORM_AUTHCheckout Form / PWI payment
BKM_AUTHBKM Express payment

When using webhooks alongside the callback, make sure you never process the same payment twice — a unique constraint on paymentId in your database is the safest way to guarantee that.

Frequently Asked Questions

Is integrating without an SDK safe?

Yes, when implemented correctly — the signing logic (HMAC-SHA256) is straightforward and well documented. The upside is no added dependency and full visibility into the exact request; the downside is you own writing and testing the signing/verification code yourself.

What's the most common cause of "invalid signature" errors?

Usually one of: parameter order not matching the endpoint, forgetting to strip trailing zeros from price/paidPrice, or the JSON body being encoded with different escaping for Turkish characters or slashes (which is exactly why the JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE flags matter).

Should I use NON-3DS or 3DS?

Prefer 3DS whenever possible: it shifts chargeback liability to the bank and reduces your PCI exposure. Some card types (where BIN lookup returns force3ds=1) only work with 3DS in the first place.

Payment Integration Support

Get professional support for your payment flow in PHP, Node.js, or another stack. Get a Quote

WhatsApp