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>
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
| Method | Flow | When to Use |
|---|---|---|
| NON-3DS | Single request, no 3DS step | Low-value, low-risk transactions |
| 3DS | Two-step: initialize → auth, requires bank OTP | Transactions where you want to reduce chargeback risk |
| Checkout Form | 4 display modes (redirect/responsive/popup/iframe), card data never touches your server | Customizable flows, smaller PCI scope |
| PWI (Pay With iyzico) | One-click payment via iyzico account, redirect only | Buyer 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']);
}
price and paidPrice (10.50 → 10.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.
| fraudStatus | Meaning | Action |
|---|---|---|
| 1 | Approved | Safe to ship the product |
| 0 | Under review | Wait, it resolves automatically |
| -1 | Rejected | Do 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:
| iyziEventType | Payment Method |
|---|---|
| API_AUTH | NON-3DS payment |
| THREE_DS_AUTH | 3DS payment |
| CHECKOUTFORM_AUTH | Checkout Form / PWI payment |
| BKM_AUTH | BKM 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.
Get professional support for your payment flow in PHP, Node.js, or another stack. Get a Quote