Framework-agnostic PHP SDK for the Kora payment API.
- PHP 8.2+
ext-openssl,ext-json
composer require mosesadewale/kora-phpGet your API keys from the Kora dashboard. Use sk_test_ keys for sandbox and sk_live_ keys for production.
use Kora\Sdk\Factory;
$kora = Factory::make(
secretKey: env('KORA_SECRET_KEY'),
encryptionKey: env('KORA_ENCRYPTION_KEY'), // required for card payments
webhookSecret: env('KORA_WEBHOOK_SECRET'),
);use Kora\Sdk\Enums\Environment;
use Kora\Sdk\Factory;
$kora = Factory::make(
secretKey: 'sk_live_...',
encryptionKey: '...', // 32-byte key, required for card payments
webhookSecret: 'wh_...',
environment: Environment::Live, // default; use Environment::Sandbox with sk_test_ keys
timeout: 30.0, // seconds, default 30
retryAttempts: 3, // retries on 5xx, default 3
);
sk_live_keys must be used withEnvironment::Live;sk_test_keys withEnvironment::Sandbox. A mismatch throwsInvalidArgumentExceptionat construction time.
Alternatively, build from a config object:
use Kora\Sdk\Factory;
use Kora\Sdk\Support\KoraConfig;
$config = new KoraConfig(secretKey: 'sk_live_...', retryAttempts: 5);
$kora = Factory::fromConfig($config, logger: $psrLogger);// Charge — returns a checkout URL for redirect-based flows, or null for direct card charges
$charge = $kora->charges()->charge([
'reference' => 'ref_' . uniqid(),
'amount' => 5000,
'currency' => 'NGN',
'customer' => ['email' => 'user@example.com', 'name' => 'Ada Okonkwo'],
'redirect_url' => 'https://yourapp.com/callback',
]);
echo $charge->checkoutUrl; // string|null — null for direct card charges
echo $charge->reference;
echo $charge->status;
// Verify a charge
$charge = $kora->charges()->verify('ref_001');Card encryption requires
encryptionKeyto be exactly 32 bytes. ALogicExceptionis thrown if it is missing or empty.
Pass a card object inside payment_options. The SDK encrypts it transparently using AES-256-GCM before sending — your code never touches the encrypted form.
$charge = $kora->charges()->charge([
'reference' => 'ref_' . uniqid(),
'amount' => 5000,
'currency' => 'NGN',
'customer' => ['email' => 'user@example.com'],
'payment_options' => [
'card' => [
'number' => '5399831111111111',
'cvv' => '100',
'expiry_month' => '10',
'expiry_year' => '31',
'pin' => '1234',
],
],
]);$charge = $kora->mobileMoney()->charge([
'reference' => 'ref_' . uniqid(),
'amount' => 5000,
'currency' => 'KES',
'customer' => ['email' => 'user@example.com'],
'payment_options' => [
'mobile_money' => ['operator' => 'mpesa', 'mobile_number' => '254712345678'],
],
]);$payout = $kora->payouts()->disburse([
'reference' => 'payout_' . uniqid(),
'destination' => [
'type' => 'bank_account',
'amount' => 10000,
'currency' => 'NGN',
'narration' => 'Freelancer payment',
'bank_account' => ['bank' => '058', 'account' => '0123456789'],
],
]);
echo $payout->reference;
echo $payout->status;$bulk = $kora->bulkPayouts()->disburse([
'reference' => 'bulk_' . uniqid(),
'currency' => 'NGN',
'payouts' => [
['reference' => 'item_1', 'amount' => 5000, 'bank_account' => ['bank' => '058', 'account' => '0123456789']],
['reference' => 'item_2', 'amount' => 3000, 'bank_account' => ['bank' => '033', 'account' => '9876543210']],
],
]);
echo $bulk->reference;
echo $bulk->totalAmount;
// Retrieve individual payout items
$items = $kora->bulkPayouts()->items($bulk->reference);$balances = $kora->balances()->list();
foreach ($balances as $balance) {
echo $balance['currency'] . ': ' . $balance['available_balance'];
}// Fetch exchange rates
$rates = $kora->conversions()->rates([
'from' => 'USD',
'to' => 'NGN',
]);
// Initiate a conversion
$conversion = $kora->conversions()->initiate([
'reference' => 'conv_' . uniqid(),
'source_currency' => 'USD',
'destination_currency' => 'NGN',
'amount' => 100,
]);
echo $conversion->sourceAmount;
echo $conversion->destinationAmount;
echo $conversion->sourceCurrency;
echo $conversion->destinationCurrency;$refund = $kora->refunds()->initiate([
'reference' => 'refund_' . uniqid(),
'transaction_reference' => 'ref_001',
'amount' => 5000,
]);
echo $refund->reference;
echo $refund->status;
// List refunds
$refunds = $kora->refunds()->list();$account = $kora->poolAccounts()->create([
'reference' => 'pool_' . uniqid(),
'name' => 'Escrow Account',
'currency' => 'NGN',
'customer' => ['email' => 'user@example.com', 'name' => 'Ada Okonkwo'],
]);
echo $account->reference;
echo $account->currency;// List chargebacks
$chargebacks = $kora->chargebacks()->list();
// Accept a chargeback
$result = $kora->chargebacks()->accept('chb_ref_001');
// Dispute a chargeback with evidence
$result = $kora->chargebacks()->dispute('chb_ref_001', [
'reason' => 'Item was delivered on 2026-04-10',
'evidence' => 'https://cdn.yourapp.com/delivery-proof.pdf',
]);$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_KORAPAY_SIGNATURE'] ?? '';
if (!$kora->webhooks()->verify($raw, $signature)) {
http_response_code(401);
exit;
}
$event = $kora->webhooks()->parse($raw);
match ($event->type) {
\Kora\Sdk\Enums\WebhookEventType::ChargeSuccess => fulfillOrder($event->data['reference']),
\Kora\Sdk\Enums\WebhookEventType::PayoutSuccess => markPaid($event->data['reference']),
\Kora\Sdk\Enums\WebhookEventType::RefundSuccess => processRefund($event->data['reference']),
default => null,
};
http_response_code(200);Supported event types:
| Constant | Kora event |
|---|---|
WebhookEventType::ChargeSuccess |
charge.success |
WebhookEventType::ChargeFailed |
charge.failed |
WebhookEventType::PayoutSuccess |
transfer.success |
WebhookEventType::PayoutFailed |
transfer.failed |
WebhookEventType::RefundSuccess |
refund.success |
WebhookEventType::RefundFailed |
refund.failed |
WebhookEventType::ChargebackCreated |
chargeback.created |
WebhookEventType::ChargebackWon |
chargeback.won |
WebhookEventType::ChargebackLost |
chargeback.lost |
$event->typeis?WebhookEventType— unknown future event types map tonullrather than throwing.
use Kora\Sdk\Exceptions\ApiException;
use Kora\Sdk\Exceptions\AuthenticationException;
use Kora\Sdk\Exceptions\DuplicateReferenceException;
use Kora\Sdk\Exceptions\InsufficientFundsException;
use Kora\Sdk\Exceptions\KoraException;
use Kora\Sdk\Exceptions\NetworkException;
use Kora\Sdk\Exceptions\ValidationException;
try {
$kora->payouts()->disburse($payload);
} catch (DuplicateReferenceException $e) {
// reference already used — check existing payout status
} catch (InsufficientFundsException $e) {
// wallet balance too low
} catch (ValidationException $e) {
// $e->errors() returns the field-level errors array
logger()->warning('Kora validation', $e->errors());
} catch (AuthenticationException $e) {
// invalid or revoked secret key
} catch (ApiException $e) {
// 5xx from Kora — $e->context() returns the raw response body
logger()->error('Kora server error', ['context' => $e->context()]);
} catch (NetworkException $e) {
// connectivity, timeout, or non-JSON response
} catch (KoraException $e) {
// catch-all for any other SDK exception
}| Exception | HTTP status | Notes |
|---|---|---|
AuthenticationException |
401 | Invalid or revoked key |
ValidationException |
400 | errors() returns field-level detail |
InsufficientFundsException |
400 | Extends ValidationException |
DuplicateReferenceException |
409 | |
ApiException |
5xx | context() returns raw response body |
NetworkException |
— | Connectivity, timeout, or non-JSON response |
Inject FakeHttpClient to test your application code without hitting the network:
use Kora\Sdk\Enums\Environment;
use Kora\Sdk\Factory;
use Kora\Sdk\Support\KoraConfig;
use Kora\Sdk\Tests\Fakes\FakeHttpClient;
$http = new FakeHttpClient(['data' => ['reference' => 'ref_001', 'status' => 'success']]);
$client = Factory::withClient(
$http,
new KoraConfig(secretKey: 'sk_test_key', environment: Environment::Sandbox),
);
$charge = $client->charges()->verify('ref_001');
self::assertSame('ref_001', $charge->reference);
self::assertCount(1, $http->requests);
self::assertSame('GET', $http->requests[0]['method']);
self::assertStringContainsString('ref_001', $http->requests[0]['uri']);FakeHttpClient records every outbound request in $http->requests as ['method', 'uri', 'options']. Queue multiple responses by passing an array; the last response is replayed when the queue is exhausted.
See mosesadewale/kora-laravel for the Laravel service provider, facade, and webhook pipeline.
MIT