Initial release: qbirr/sdk v1.0.0

This commit is contained in:
2026-07-17 16:42:30 +02:00
commit ce11a3b94d
3 changed files with 270 additions and 0 deletions
+68
View File
@@ -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
+18
View File
@@ -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/"
}
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
/**
* qbirr — PHP client for the qbirr payment verification API.
*
* $qb = new Qbirr\Qbirr('your_api_key');
* $r = $qb->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;
}
}