A PHP SDK for integrating the Halkode payment gateway into your application. Designed as a Yii2-compatible Composer package with clean abstraction for token storage, encryption, and multi-language response handling.
📖 Official Integration Docs: This SDK is built based on the official Halkode Integration Documentation. Refer to it for detailed API specifications, parameter descriptions, and response formats.
- Requirements
- Installation
- Configuration
- Quick Start
- API Reference
- Payment Workflows
- Token Storage
- Error Handling
- Testing
- Project Structure
- License
- PHP >= 8.1
- yiisoft/yii2 ~2.0.0
- yiisoft/yii2-httpclient ~2.0.0
composer require mskayali/halkodeAdd the following to your config/web.php or config/main.php:
'components' => [
'halkode' => [
'class' => \mskayali\halkode\Client::class,
'baseUrl' => \mskayali\halkode\Client::BASE_URL_DEV, // or BASE_URL_PROD
'merchantUID' => 'your-merchant-uid',
'apikeypublic' => 'your-api-secret-key',
'apiclientpublic' => 'your-api-client-key',
'language' => 'tr', // 'tr' or 'en' for response messages
// Optional: custom token storage (see Token Storage section)
// 'tokenStorage' => new RedisTokenStorage($redis),
],
],use mskayali\halkode\Client;
$client = new Client([
'baseUrl' => Client::BASE_URL_DEV,
'merchantUID' => 'your-merchant-uid',
'apikeypublic' => 'your-api-secret-key',
'apiclientpublic' => 'your-api-client-key',
]);
$services = $client->getPaymentServices();| Environment | Constant | URL |
|---|---|---|
| Development | Client::BASE_URL_DEV |
https://testapp.halkode.com.tr/ccpayment |
| Production | Client::BASE_URL_PROD |
https://app.halkode.com.tr/ccpayment |
use mskayali\halkode\Client;
use mskayali\halkode\models\PurchaseLinkRequest;
use mskayali\halkode\models\Invoice;
use mskayali\halkode\models\InvoiceItem;
// 1. Create client
$client = new Client([
'baseUrl' => Client::BASE_URL_DEV,
'merchantUID' => 'your-merchant-uid',
'apikeypublic' => 'your-api-secret-key',
'apiclientpublic' => 'your-api-client-key',
]);
// 2. Build invoice
$invoice = new Invoice([
'invoice_id' => 'INV-' . time(),
'invoice_description' => 'Order #123',
'total' => 250.00,
'return_url' => 'https://example.com/payment/success',
'cancel_url' => 'https://example.com/payment/cancel',
'items' => [
new InvoiceItem(['name' => 'Widget', 'price' => 250, 'quantity' => 1]),
],
]);
// 3. Create payment link
$services = $client->getPaymentServices();
$result = $services->purchaseLink(new PurchaseLinkRequest([
'currency_code' => 'TRY',
'invoice' => $invoice,
'name' => 'John',
'surname' => 'Doe',
]));
if ($result->success) {
// Redirect user to the payment page
header('Location: ' . $result->data->link);
}All services are accessed through $client->getPaymentServices():
| Method | Endpoint | Description |
|---|---|---|
purchaseLink($request) |
/purchase/link |
Generate a hosted payment page link |
paySmart2D($request) |
/api/paySmart2D |
Direct card payment (no 3D Secure) |
paySmart3D($request) |
/api/paySmart3D |
Card payment with 3D Secure verification |
refundPayment($request) |
/api/refund |
Full or partial refund |
checkstatus($request) |
/api/checkstatus |
Query transaction status |
confirmPayment($request) |
/api/confirmPayment |
Confirm a pre-authorized payment |
getInstallment($request) |
/api/installments |
Query available installment options |
merchantCommissions($request) |
/api/commissions |
Retrieve merchant commission rates |
getPos($request) |
/api/getpos |
Query available POS terminals for a card |
getTransaction($request) |
/api/getTransactions |
Retrieve a single transaction |
allTransaction($request) |
/api/alltransaction |
List all transactions |
paymentComplete($request) |
/payment/complete |
Finalize a payment |
Every service method returns an object with these attributes:
$result->success; // bool — whether the operation succeeded
$result->message; // string|null — localized status message (based on Client language)
$result->data; // object|null — response data on success
$result->error; // mixed|null — error details on failureBest for: redirecting users to a Halkode-hosted payment page.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Server │────▶│ Halkode │────▶│ Customer │
│ (Your │ │ purchase │ │ Hosted │ │ completes│
│ App) │◀────│ Link() │◀────│ Page │◀────│ payment │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
redirect to return_url
or cancel_url
// Step 1: Create payment link
$result = $services->purchaseLink(new PurchaseLinkRequest([
'currency_code' => 'TRY',
'invoice' => $invoice,
'name' => 'John',
'surname' => 'Doe',
]));
// Step 2: Redirect user
if ($result->success) {
return redirect($result->data->link);
}
// Step 3: Handle callback (on your return_url)
// Halkode redirects back with payment result in POST data
$invoiceId = $_POST['invoice_id'];
$status = $services->checkstatus(new CheckstatusRequest([
'invoice_id' => $invoiceId,
'merchant_key' => $client->getMerchantUID(),
]));
if ($status->success) {
// Payment confirmed — update your order
}Best for: collecting card data on your own PCI-compliant form.
// Step 1: Charge the card directly
$result = $services->paySmart2D(new PaySmart2DRequest([
'cc_holder_name' => 'John Doe',
'cc_no' => '4155141122223339',
'expiry_month' => '12',
'expiry_year' => '26',
'cvv' => '555',
'currency_code' => 'TRY',
'installments_number' => 1,
'invoice_id' => 'INV-' . time(),
'invoice_description' => 'Premium subscription',
'total' => 99.00,
'name' => 'John',
'surname' => 'Doe',
]));
// Step 2: Check result
if ($result->success) {
$orderNo = $result->data->order_no;
// Save $orderNo to your database for future reference
} else {
$error = $result->error;
// Display error to user or log
}Best for: most production card payments — adds an extra bank verification step.
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Your │────▶│ Halkode │────▶│ Bank 3D │────▶│ Halkode │
│ Server │ │ API │ │ Verify │ │ Callback │
│ paySmart │ │ returns │ │ Page │ │ return │
│ 3D() │ │ redirect │ │ (OTP) │ │ _url │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
┌─────▼─────┐
│ Your App │
│ handles │
│ callback │
└───────────┘
// Step 1: Initiate 3D payment
$result = $services->paySmart3D(new PaySmart3DRequest([
'cc_holder_name' => 'John Doe',
'cc_no' => '4155141122223339',
'expiry_month' => '12',
'expiry_year' => '26',
'cvv' => '555',
'currency_code' => 'TRY',
'installments_number' => 1,
'invoice_id' => 'INV-3D-' . time(),
'total' => 500.00,
'name' => 'John',
'surname' => 'Doe',
'return_url' => 'https://example.com/payment/3d-callback',
'cancel_url' => 'https://example.com/payment/cancel',
]));
// Step 2: Redirect user to 3D verification
if ($result->success) {
return redirect($result->data->redirect_url);
}
// Step 3: Handle 3D callback (on your return_url)
// The bank redirects back after OTP verification
$invoiceId = $_POST['invoice_id'] ?? $_GET['invoice_id'];
// Step 4: Verify the payment status
$status = $services->checkstatus(new CheckstatusRequest([
'invoice_id' => $invoiceId,
'merchant_key' => $client->getMerchantUID(),
]));
if ($status->success && $status->data->payment_status == 1) {
// Payment successful — fulfill the order
} else {
// Payment failed or pending
}Best for: reserving funds first, then capturing later (e.g., hotel bookings, car rentals).
// Step 1: Pre-authorize (hold funds on the card)
$result = $services->paySmart2D(new PaySmart2DRequest([
'cc_holder_name' => 'John Doe',
'cc_no' => '4155141122223339',
'expiry_month' => '12',
'expiry_year' => '26',
'cvv' => '555',
'currency_code' => 'TRY',
'installments_number' => 1,
'invoice_id' => 'PREAUTH-' . time(),
'total' => 1000.00,
'name' => 'John',
'surname' => 'Doe',
'transaction_type' => TransactionType::PreAuthorization,
]));
// Step 2: Later — confirm (capture) the payment
if ($result->success) {
$confirmed = $services->confirmPayment(new ConfirmPaymentRequest([
'invoice_id' => 'PREAUTH-XXX',
'total' => '1000.00',
'status' => '1',
'merchant_key' => $client->getMerchantUID(),
'hash_key' => $client->generateHash('PREAUTH-XXX'),
]));
if ($confirmed->success) {
// Funds captured — order finalized
}
}// Full refund
$result = $services->refundPayment(new RefundAPIRequest([
'invoice_id' => 'INV-ORIGINAL',
'amount' => 99.00, // Full amount
'app_id' => $client->apiclientpublic,
'app_secret' => $client->apikeypublic,
'merchant_key' => $client->getMerchantUID(),
'hash_key' => $client->generateHash('INV-ORIGINAL'),
]));
// Partial refund (e.g., 30 TRY of a 99 TRY charge)
$partialRefund = $services->refundPayment(new RefundAPIRequest([
'invoice_id' => 'INV-ORIGINAL',
'amount' => 30.00, // Partial amount
'app_id' => $client->apiclientpublic,
'app_secret' => $client->apikeypublic,
'merchant_key' => $client->getMerchantUID(),
'hash_key' => $client->generateHash('INV-ORIGINAL'),
]));// Check a specific transaction
$tx = $services->getTransaction(new GetTransactionRequest([
'invoice_id' => 'INV-001',
'merchant_key' => $client->getMerchantUID(),
'hash_key' => $client->generateHash('INV-001'),
]));
// List all transactions (for daily reconciliation)
$allTx = $services->allTransaction(new AllTransactionRequest([
'merchant_key' => $client->getMerchantUID(),
]));
// Check payment status (polling for async flows)
$status = $services->checkstatus(new CheckstatusRequest([
'invoice_id' => 'INV-001',
'merchant_key' => $client->getMerchantUID(),
]));Best for: showing available installment options to the user before payment.
// Step 1: Query installments for the card BIN
$installments = $services->getInstallment(new GetInstallmentRequest([
'bin_number' => substr($cardNumber, 0, 6), // First 6 digits
'total' => 1200.00,
'currency_code' => 'TRY',
]));
// Step 2: Display installment options to user
if ($installments->success) {
foreach ($installments->data as $option) {
echo "Installments: {$option->installment_number} "
. "— Monthly: {$option->monthly_payment} TRY "
. "— Total: {$option->total_amount} TRY\n";
}
}
// Step 3: User selects installment count, then call paySmart2D/3D
$result = $services->paySmart3D(new PaySmart3DRequest([
'installments_number' => 6, // User's selection
// ... other fields
]));Query available POS terminals for a card:
$pos = $services->getPos(new GetPosRequest([
'credit_card' => '415514', // First 6 digits (BIN)
'amount' => 500.00,
'currency_code' => 'TRY',
'merchant_key' => $client->getMerchantUID(),
]));
if ($pos->success) {
// pos->data contains available POS terminals with:
// - pos_id, card_type, card_scheme
// - payable_amount, hash_key
// - commission_rate, installment details
}The SDK automatically manages API authentication tokens. By default, tokens are stored in-memory and discarded after each request.
// No configuration needed — MemoryTokenStorage is used automatically
$client = new Client([...]);For persistent token caching across requests:
use mskayali\halkode\TokenStorageInterface;
class RedisTokenStorage implements TokenStorageInterface
{
private \Redis $redis;
public function __construct(\Redis $redis)
{
$this->redis = $redis;
}
public function get(string $key): ?string
{
$value = $this->redis->get($key);
return $value === false ? null : $value;
}
public function set(string $key, string $value, int $ttl): bool
{
return $this->redis->setex($key, $ttl, $value);
}
}
// Usage:
$client = new Client([
// ... credentials
'tokenStorage' => new RedisTokenStorage($redis),
]);
⚠️ Security Note: API tokens stored in Redis are sensitive credentials. If your Redis instance is shared, accessible over a network, or not configured with TLS, you should encrypt tokens before storing them. This prevents token leakage if Redis is compromised.
use mskayali\halkode\TokenStorageInterface;
class EncryptedRedisTokenStorage implements TokenStorageInterface
{
private \Redis $redis;
private string $encryptionKey;
private string $cipher = 'aes-256-cbc';
/**
* @param \Redis $redis Connected Redis instance
* @param string $encryptionKey A 32-byte (256-bit) secret key.
* Generate once with: bin2hex(random_bytes(16))
* Store securely (e.g., environment variable).
*/
public function __construct(\Redis $redis, string $encryptionKey)
{
$this->redis = $redis;
$this->encryptionKey = $encryptionKey;
}
public function get(string $key): ?string
{
$encrypted = $this->redis->get($key);
if ($encrypted === false) {
return null;
}
$data = base64_decode($encrypted);
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = substr($data, 0, $ivLength);
$ciphertext = substr($data, $ivLength);
$decrypted = openssl_decrypt($ciphertext, $this->cipher, $this->encryptionKey, OPENSSL_RAW_DATA, $iv);
return $decrypted === false ? null : $decrypted;
}
public function set(string $key, string $value, int $ttl): bool
{
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cipher));
$ciphertext = openssl_encrypt($value, $this->cipher, $this->encryptionKey, OPENSSL_RAW_DATA, $iv);
$encrypted = base64_encode($iv . $ciphertext);
if ($ttl > 0) {
return $this->redis->setex($key, $ttl, $encrypted);
}
return $this->redis->set($key, $encrypted);
}
}Setup:
// 1. Generate an encryption key ONCE and store it securely:
// php -r "echo bin2hex(random_bytes(16));"
// → e.g., "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
// 2. Store the key in an environment variable:
// HALKODE_TOKEN_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
// 3. Use in your application:
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$client = new Client([
'baseUrl' => Client::BASE_URL_PROD,
'merchantUID' => 'your-merchant-uid',
'apikeypublic' => 'your-api-secret-key',
'apiclientpublic' => 'your-api-client-key',
'tokenStorage' => new EncryptedRedisTokenStorage(
$redis,
getenv('HALKODE_TOKEN_KEY')
),
]);When to use which:
| Storage | Use Case |
|---|---|
MemoryTokenStorage (default) |
Development, single-request scripts, stateless apps |
RedisTokenStorage (plain) |
Production with secured Redis (TLS, private network, AUTH) |
EncryptedRedisTokenStorage |
Production with shared/untrusted Redis, compliance requirements |
Any class implementing TokenStorageInterface can be passed to the client:
interface TokenStorageInterface
{
public function get(string $key): ?string;
public function set(string $key, string $value, int $ttl): bool;
}Every service call returns a response object. No exceptions are thrown for API errors — errors are returned in the response:
$result = $services->paySmart2D($request);
if (!$result->success) {
// Option 1: Localized message
echo $result->message; // e.g., "Authentication error" (en) or "Kimlik doğrulama hatası" (tr)
// Option 2: Detailed error
if (is_array($result->error)) {
foreach ($result->error as $field => $messages) {
echo "$field: " . implode(', ', $messages) . "\n";
}
} else {
echo $result->error;
}
}| Code | English | Turkish |
|---|---|---|
| 200 | OK | Tamam |
| 201 | Created | Oluşturuldu |
| 400 | Bad Request | Hatalı istek |
| 401 | Authentication Error | Kimlik doğrulama hatası |
| 403 | Forbidden | Yasak |
| 404 | Resource Not Found | Kaynak bulunamadı |
| 500 | Internal Server Error | Dahili Sunucu Hatası |
Run the test suite:
composer install
./vendor/bin/phpunit --testdoxThe test suite includes 108 tests with 218 assertions covering:
| Suite | Tests | Coverage |
|---|---|---|
| AutoloadTest | 36 | PSR-4 autoloading for all classes |
| MemoryTokenStorageTest | 7 | Interface, CRUD, TTL, overwrites |
| ClientTest | 17 | Constants, encryption, tokens, auth headers |
| BaseResponseTest | 8 | Language support, status codes |
| ModelTest | 26 | Validation rules, serialization |
| PaymentServicesTest | 14 | All 12 endpoint methods |
mskayali/halkode/
├── composer.json # Package definition & PSR-4 autoload
├── phpunit.xml # PHPUnit configuration
├── README.md
├── examples/
│ ├── all_services.php # Working examples for all 12 endpoints
│ ├── redis_token_storage.php # Custom Redis token storage
│ └── yii2_integration.php # Yii2 component & controller patterns
├── src/
│ ├── Client.php # HTTP client, encryption, token management
│ ├── TokenStorageInterface.php
│ ├── MemoryTokenStorage.php
│ ├── models/
│ │ ├── BaseResponse.php
│ │ ├── StatusCodes.php
│ │ ├── TransactionType.php
│ │ ├── Invoice.php
│ │ ├── InvoiceItem.php
│ │ ├── GetTokenRequest.php / GetTokenBody.php
│ │ ├── PurchaseLinkRequest.php / PurchaseLinkBody.php
│ │ ├── PaySmart2DRequest.php / PaySmart2DBody.php / PaySmart2DData.php
│ │ ├── PaySmart3DRequest.php / PaySmart3DBody.php
│ │ ├── RefundAPIRequest.php / RefundAPIBody.php
│ │ ├── CheckstatusRequest.php / CheckstatusBody.php
│ │ ├── ConfirmPaymentRequest.php / ConfirmPaymentBody.php
│ │ ├── GetInstallmentRequest.php / GetInstallmentBody.php
│ │ ├── MerchantCommissionsRequest.php / MerchantCommissionsBody.php
│ │ ├── GetPosRequest.php / GetPosBody.php / GetPosData.php
│ │ ├── GetTransactionRequest.php / GetTransactionBody.php
│ │ ├── AllTransactionRequest.php / AllTransactionBody.php
│ │ └── PaymentCompleteRequest.php / PaymentCompleteBody.php
│ └── services/
│ └── PaymentServices.php # All 12 API endpoint methods
└── tests/
├── bootstrap.php
├── AutoloadTest.php
├── MemoryTokenStorageTest.php
├── ClientTest.php
├── BaseResponseTest.php
├── ModelTest.php
└── PaymentServicesTest.php
MIT