Payment Integration with Laravel (iyzico): A Real-World Experience
Real problems and solutions encountered while integrating iyzico payments into a Laravel project — things not in the documentation.
Payment integration looks straightforward in theory: read the API docs, send the required parameters, handle the response. In practice, something unexpected comes up at every step. In this post I’m sharing what I learned while integrating iyzico into an e-commerce project — things that aren’t in the documentation but that I ran into in the field.
Why iyzico?
There are a few options for card payments in Turkey. I chose iyzico because it had a PHP SDK built for Turkish developers and a sandbox environment that actually worked. At the time, international alternatives like Stripe had more complex integration paths with Turkish banks.
Installing the SDK via Composer is straightforward:
composer require iyzipay/iyzipay-php
The Payment Flow
iyzico’s basic payment flow consists of these steps:
- Generate a payment form (iframe or redirect sent to the client).
- The user enters their card details (on iyzico’s own interface).
- Verify the payment result using a token.
In the 3D Secure flow, once the transaction completes, iyzico sends a POST request to your callbackUrl. At that point your application needs to handle that request correctly.
use Iyzipay\Options;
use Iyzipay\Model\Payment;
use Iyzipay\Request\CreatePaymentRequest;
$options = new Options();
$options->setApiKey(config('services.iyzico.api_key'));
$options->setSecretKey(config('services.iyzico.secret_key'));
$options->setBaseUrl(config('services.iyzico.base_url'));
$request = new CreatePaymentRequest();
$request->setLocale('tr');
$request->setConversationId((string) $order->id);
$request->setPrice($order->subtotal);
$request->setPaidPrice($order->total);
$request->setCurrency('TRY');
The conversationId field is critical: you put your own order ID here. iyzico sends this value back in the callback, so you can match the payment to the correct order.
The Callback: Where You Need to Be Most Careful
In the 3D Secure flow, after the user passes the bank’s verification screen, iyzico sends a POST request to your application’s callbackUrl. What you need to do at this point:
- Verify the payment result from the iyzico API using the
paymentId. - Update the order.
- Redirect the user to the appropriate page.
The most important thing I learned here is this: your callback URL must be reliable. The user might close the browser or lose their connection — in those cases the payment may have gone through but the order may not have been updated. That’s why it’s important to keep the callback handler synchronous rather than dispatching to a queue, and to keep it as short as possible. Just save the payment status; emails and other processing can wait in the queue.
public function callback(Request $request)
{
$token = $request->input('token');
$retrieve = new RetrieveCheckoutFormResultRequest();
$retrieve->setToken($token);
$retrieve->setLocale('tr');
$result = CheckoutForm::retrieve($retrieve, $this->options);
if ($result->getPaymentStatus() === 'SUCCESS') {
$order = Order::where('iyzico_token', $token)->firstOrFail();
$order->markAsPaid($result->getPaymentId());
}
return redirect()->route('orders.show', $order);
}
Differences Between Sandbox and Production
The sandbox environment works well overall, but I noticed two differences:
Response times: The sandbox is sometimes slower than production. You need to set timeout values accordingly; something that works in the sandbox may behave differently in production.
3D Secure testing: To test the 3D Secure flow in the sandbox you use iyzico’s test card numbers. These cards don’t always behave exactly as documented; setting up the specific scenario you want to test (success, failure, insufficient balance) with the right card number can take some time.
Log Your Error Messages
iyzico’s error codes in its responses aren’t always descriptive enough. During integration, setting up a structure that logs both requests and responses saved a lot of time. In production, sensitive card data should never end up in logs — but if the payment ID, order ID, and iyzico’s returned error code are in the logs, diagnosing problems becomes much easier.
Payment integration is a patience-testing job. The basic flow was up in two days, but closing the edge cases (repeated callbacks, duplicate payments, timeouts) took a week. Not having written tests for specific scenarios before spending that week became apparent over time — a lesson that paid off in subsequent integrations.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.