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; } }