mosesadewale / kora-php
Framework-agnostic PHP SDK for the Kora payment API (formerly KoraPay).
Requires
- php: ^8.2
- ext-json: *
- ext-openssl: *
- guzzlehttp/guzzle: ^7.8
- psr/log: ^3.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-29 00:05:38 UTC
README
Framework-agnostic PHP SDK for the Kora payment API.
Requirements
- PHP 8.2+
ext-openssl,ext-json
Installation
composer require mosesadewale/kora-php
Quick start
Get 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'), );
Configuration
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);
Resources
Charges
// 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 charges
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', ], ], ]);
Mobile Money
$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'], ], ]);
Payouts
$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 Payouts
$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
$balances = $kora->balances()->list(); foreach ($balances as $balance) { echo $balance['currency'] . ': ' . $balance['available_balance']; }
Conversions
// 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;
Refunds
$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();
Pool Accounts
$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;
Chargebacks
// 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', ]);
Webhooks
$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.
Error handling
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 |
Testing
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.
Laravel
See mosesadewale/kora-laravel for the Laravel service provider, facade, and webhook pipeline.
License
MIT