commit ce11a3b94d4094b71b5d8d7348080ec563ff37e1 Author: admin Date: Fri Jul 17 16:42:30 2026 +0200 Initial release: qbirr/sdk v1.0.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8525b96 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# qbirr/sdk (PHP) + +Verify Ethiopian bank transfer receipts in real time. + +## Install + +```bash +composer require qbirr/sdk +``` + +Requires PHP 7.4+ and the `curl` + `json` extensions (standard on almost every host). + +## Quick start + +```php +use Qbirr\Qbirr; + +$qb = new Qbirr($_ENV['QBIRR_API_KEY']); + +$r = $qb->verify([ + 'provider' => 'cbe', + 'ref' => 'FT23001234ABC', + 'amount' => 500, + 'receiver_name' => 'YOUR FULL NAME', + 'receiver_account' => '1000017692643', +]); + +if ($r['verified']) { + echo "Paid by " . $r['payer'] . ", amount: " . $r['amount']; +} else { + echo "Failed: " . $r['error']; +} +``` + +## Drop-in for WordPress / Laravel / vanilla PHP + +Works the same in every framework — it's just curl under the hood. + +```php +// WordPress / WooCommerce — inside a payment gateway +$r = (new Qbirr\Qbirr(get_option('qbirr_api_key')))->verify([...]); + +// Laravel +$r = (new \Qbirr\Qbirr(config('services.qbirr.key')))->verify([...]); + +// Vanilla +require 'vendor/autoload.php'; +$r = (new Qbirr\Qbirr(getenv('QBIRR_API_KEY')))->verify([...]); +``` + +## Errors + +```php +use Qbirr\Qbirr; +use Qbirr\QbirrException; + +try { + $r = $qb->verify([ ... ]); +} catch (QbirrException $e) { + error_log("qbirr error " . $e->status . ": " . $e->getMessage()); +} +``` + +Verification failures (wrong amount, wrong receiver) are **not** exceptions — they're returned as `['verified' => false, 'error' => '...']`. + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..934a46b --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "qbirr/sdk", + "description": "qbirr.com — Ethiopian bank verification SDK (CBE, Telebirr, Awash, Dashen, M-Pesa, Abyssinia, eBirr)", + "type": "library", + "license": "MIT", + "keywords": ["ethiopia", "cbe", "telebirr", "awash", "dashen", "mpesa", "abyssinia", "ebirr", "payment", "verification"], + "homepage": "https://qbirr.com", + "require": { + "php": ">=7.4", + "ext-curl": "*", + "ext-json": "*" + }, + "autoload": { + "psr-4": { + "Qbirr\\": "src/" + } + } +} diff --git a/src/Qbirr.php b/src/Qbirr.php new file mode 100644 index 0000000..ae3aea8 --- /dev/null +++ b/src/Qbirr.php @@ -0,0 +1,184 @@ +verify([ + * 'provider' => 'cbe', + * 'ref' => 'FT23001234ABC', + * 'amount' => 500, + * 'receiver_name' => 'YOUR NAME', + * 'receiver_account' => '1000017692643', + * ]); + * if ($r['verified']) echo "Paid by " . $r['payer']; + */ + +declare(strict_types=1); + +namespace Qbirr; + +class QbirrException extends \RuntimeException { + public ?int $status; + public $body; + public function __construct(string $msg, ?int $status = null, $body = null) { + parent::__construct($msg); + $this->status = $status; + $this->body = $body; + } +} + +class Qbirr { + public const VERSION = '1.0.0'; + public const DEFAULT_BASE_URL = 'https://verify.qbirr.com'; + + private string $apiKey; + private string $baseUrl; + private int $timeoutSec; + + public function __construct(string $apiKey, string $baseUrl = self::DEFAULT_BASE_URL, int $timeoutSec = 30) { + if ($apiKey === '') throw new QbirrException('apiKey is required'); + $this->apiKey = $apiKey; + $this->baseUrl = rtrim($baseUrl, '/'); + $this->timeoutSec = $timeoutSec; + } + + /** + * Verify a transaction. + * + * @param array $req { + * provider: cbe | telebirr | abyssinia | dashen | mpesa | awash | ebirr + * ref: Transaction ref. For awash, SMS URL or token. For ebirr, "tenant/token" or URL. + * amount: Expected payment in ETB. + * receiver_name: Your receiving account name. + * receiver_account: Required for cbe and abyssinia. + * } + * @return array { verified: bool, payer: string, amount: float, error: string } + */ + public function verify(array $req): array { + $body = [ + 'provider' => $req['provider'] ?? '', + 'ref' => $req['ref'] ?? '', + 'amount' => $req['amount'] ?? 0, + 'receiver_name' => $req['receiver_name'] ?? '', + ]; + if (!empty($req['receiver_account'])) $body['receiver_account'] = $req['receiver_account']; + + $data = $this->request('POST', '/api/v1/verify', $body); + return [ + 'verified' => (bool) ($data['verified'] ?? false), + 'payer' => (string) ($data['payer'] ?? ''), + 'amount' => (float) ($data['amount'] ?? 0), + 'error' => (string) ($data['error'] ?? ''), + ]; + } + + /** + * @return array { used, limit, remaining, plan, period_start, period_end } + */ + public function usage(): array { + return $this->request('GET', '/api/v1/usage'); + } + + /** + * Verify by uploading a receipt PDF or image. + * + * @param array $req { provider, amount, receiver_name, [receiver_account], filePath } + * OR { provider, ..., fileContent, fileName, mimeType } + */ + public function verifyUpload(array $req): array { + if (isset($req['filePath'])) { + $fileContent = file_get_contents($req['filePath']); + if ($fileContent === false) throw new QbirrException("Could not read file: " . $req['filePath']); + $fileName = basename($req['filePath']); + $mimeType = mime_content_type($req['filePath']) ?: 'application/octet-stream'; + } else { + $fileContent = $req['fileContent'] ?? ''; + $fileName = $req['fileName'] ?? 'upload.pdf'; + $mimeType = $req['mimeType'] ?? 'application/octet-stream'; + } + if ($fileContent === '') throw new QbirrException("Empty file content"); + + $boundary = '----qbirrphp' . bin2hex(random_bytes(8)); + $body = ''; + foreach (['provider', 'amount', 'receiver_name', 'receiver_account'] as $k) { + if (isset($req[$k]) && $req[$k] !== '') { + $body .= "--{$boundary}\r\n" + . "Content-Disposition: form-data; name=\"{$k}\"\r\n\r\n" + . $req[$k] . "\r\n"; + } + } + $body .= "--{$boundary}\r\n" + . "Content-Disposition: form-data; name=\"file\"; filename=\"{$fileName}\"\r\n" + . "Content-Type: {$mimeType}\r\n\r\n" + . $fileContent . "\r\n" + . "--{$boundary}--\r\n"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $this->baseUrl . '/api/v1/verify/upload', + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeoutSec + 30, + CURLOPT_HTTPHEADER => [ + 'X-API-Key: ' . $this->apiKey, + 'Content-Type: multipart/form-data; boundary=' . $boundary, + 'Accept: application/json', + 'User-Agent: qbirr-php/' . self::VERSION, + ], + CURLOPT_POSTFIELDS => $body, + ]); + $raw = curl_exec($ch); + $err = curl_error($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($raw === false) throw new QbirrException("Network error: $err"); + $data = json_decode((string) $raw, true); + if (!is_array($data)) throw new QbirrException("Non-JSON response (HTTP $code)", $code, $raw); + if ($code < 200 || $code >= 300) { + throw new QbirrException((string)($data['error'] ?? "HTTP $code"), $code, $data); + } + return [ + 'verified' => (bool) ($data['verified'] ?? false), + 'payer' => (string) ($data['payer'] ?? ''), + 'amount' => (float) ($data['amount'] ?? 0), + 'error' => (string) ($data['error'] ?? ''), + ]; + } + + private function request(string $method, string $path, ?array $body = null): array { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $this->baseUrl . $path, + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => $this->timeoutSec, + CURLOPT_HTTPHEADER => [ + 'X-API-Key: ' . $this->apiKey, + 'Content-Type: application/json', + 'Accept: application/json', + 'User-Agent: qbirr-php/' . self::VERSION, + ], + ]); + if ($body !== null) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + } + + $raw = curl_exec($ch); + $err = curl_error($ch); + $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($raw === false) throw new QbirrException("Network error: $err"); + + $data = json_decode((string) $raw, true); + if (!is_array($data)) { + throw new QbirrException("Non-JSON response (HTTP $code)", $code, $raw); + } + if ($code < 200 || $code >= 300) { + $msg = $data['error'] ?? "HTTP $code"; + throw new QbirrException((string) $msg, $code, $data); + } + return $data; + } +}